Deposit contract and address standard
Simple Summary
This ERC defines a simple contract interface for managing deposits. It also defines a new address format that encodes the extra data passed into the interface’s main deposit function.
Abstract
An ERC-2876 compatible deposit system can accept ETH payments from multiple depositors without the need for managing multiple keys or requiring use of a hot wallet.
An ERC-2876 compatible wallet application can send ETH to ERC-2876 compatible deposit systems in a way that the deposit system can differentiate their payment using the 8 byte id specified in this standard.
Adoption of ERC-2876 by all exchanges (as a deposit system and as a wallet for their withdrawal systems), merchants, and all wallet applications/libraries will likely decrease total network gas usage by these systems, since two value transactions cost 42000 gas while a simple ETH forwarding contract will cost closer to 30000 gas depending on the underlying implementation.
This also has the benefit for deposit system administrators of allowing for all deposits to be forwarded to a cold wallet directly without any manual operations to gather deposits from multiple external accounts.
Motivation
Centralized exchanges and merchants (Below: “apps”) require an address format for accepting deposits. Currently the address format used refers to an account (external or contract), but this creates a problem. It requires that apps create a new account for every invoice / user. If the account is external, that means the app must have the deposit addresses be hot wallets, or have increased workload for cold wallet operators (as each deposit account will create 1 value tx to sweep). If the account is contract, generating an account costs at least 60k gas for a simple proxy, which is cost-prohibitive.
Therefore, merchant and centralized exchange apps are forced between taking on one of the following:
- Large security risk (deposit accounts are hot wallets)
- Large manual labor cost (cold account manager spends time sweeping thousands of cold accounts)
- Large service cost (deploying a contract-per-deposit-address model).
The timing of this proposal is within the context of increased network gas prices. During times like this, more and more services who enter the space are being forced into hot wallets for deposits, which is a large security risk.
The motivation for this proposal is to lower the cost of deploying and managing a system that accepts deposits from many users, and by standardizing the methodology for this, services across the world can easily use this interface to send value to and from each other without the need to create multiple accounts.
Specification
Definitions
- The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
The contract interface
is the contract component of this ERC.The deposit address format
is the newly made format described in “Deposit Address Format” for encoding the 20 byte account address and the 8 byte id.The contract
refers to the contract that implementsthe contract interface
of this ERC.The 8 byte "id"
is an 8 byte id used as the input parameter for the contract interface.The 5 byte "nonce"
is the first 5 most significant bytes of the"id"
.The 3 byte "checksum"
is the last 3 least significant bytes of the"id"
deposit(bytes8)
refers to the function of that signature, which is defined inthe contract interface
.The parent application
refers to the application that will use the information gained within thedeposit(bytes8)
function. (ie. an exchange backend or a non-custodial merchant application)The depositor
refers to the person that will send value tothe contract
via thedeposit(bytes8)
call.The wallet
refers to any application or library that sends value transactions upon the request ofthe depositor
. (ie. MyEtherWallet, Ledger, blockchain.com, various libraries)
Deposit Address Format
In order to add the 8 byte “id” data, we need to encode it along with the 20 byte account address. The 8 bytes are appended to the 20 byte address.
A 3 byte checksum is included in the id, which is the first 3 bytes of the keccak256 hash of the 20 byte address and first 5 byte nonce of the id concatenated (25 bytes).
The Deposit Address format can be generated with the following JavaScript code:
/**
* Converts a 20 byte account address and a 5 byte nonce to a deposit address.
* The format of the return value is 28 bytes as follows. The + operator is byte
* concatenation.
* (baseAddress + nonce + keccak256(baseAddress + nonce)[:3])
*
* @param {String} baseAddress the given HEX address (20 byte hex string with 0x prepended)
* @param {String} nonce the given HEX nonce (5 byte hex string with 0x prepended)
* @return {String}
*/
function generateAddress (baseAddress, nonce) {
if (
!baseAddress.match(/^0x[0-9a-fA-F]{40}$/) ||
!nonce.match(/^0x[0-9a-fA-F]{10}$/)
) {
throw new Error('Base Address and nonce must be 0x hex strings');
}
const ret =
baseAddress.toLowerCase() + nonce.toLowerCase().replace(/^0x/, '');
const myHash = web3.utils.keccak256(ret);
return ret + myHash.slice(2, 8); // first 3 bytes from the 0x hex string
};
The checksum can be verified within the deposit contract itself using the following:
function checksumMatch(bytes8 id) internal view returns (bool) {
bytes32 chkhash = keccak256(
abi.encodePacked(address(this), bytes5(id))
);
bytes3 chkh = bytes3(chkhash);
bytes3 chki = bytes3(bytes8(uint64(id) << 40));
return chkh == chki;
}
The Contract Interface
A contract that follows this ERC:
The contract
MUST revert if sent a transaction wheremsg.data
is null (A pure value transaction).The contract
MUST have a deposit function as follows:
interface DepositEIP {
function deposit(bytes8 id) external payable returns (bool);
}
deposit(bytes8)
MUST returnfalse
when the contract needs to keep the value, but signal to the depositor that the deposit (in terms of the parent application) itself has not yet succeeded. (This can be used for partial payment, ie. the invoice is for 5 ETH, sending 3 ETH returns false, but sending a second tx with 2 ETH will return true.)deposit(bytes8)
MUST revert if the deposit somehow failed and the contract does not need to keep the value sent.deposit(bytes8)
MUST returntrue
if the value will be kept and the payment is logically considered complete by the parent application (exchange/merchant).deposit(bytes8)
SHOULD check the checksum contained within the 8 byte id. (See “Deposit Address Format” for an example)The parent application
SHOULD return any excess value received if the deposit id is a one-time-use invoice that has a set value and the value received is higher than the set value. However, this SHOULD NOT be done by sending back tomsg.sender
directly, but rather should be noted in the parent application and the depositor should be contacted out-of-band to the best of the application manager’s ability.
Depositing Value to the Contract from a Wallet
The wallet
MUST acceptthe deposit address format
anywhere the 20-byte address format is accepted for transaction destination.The wallet
MUST verify the 3 byte checksum and fail if the checksum doesn’t match.The wallet
MUST fail if the destination address isthe deposit address format
and thedata
field is set to anything besides null.The wallet
MUST set theto
field of the underlying transaction to the first 20 bytes of the deposit address format, and set thedata
field to0x3ef8e69aNNNNNNNNNNNNNNNN000000000000000000000000000000000000000000000000
whereNNNNNNNNNNNNNNNN
is the last 8 bytes of the deposit address format. (ie. if the deposit address format is set to0x433e064c42e87325fb6ffa9575a34862e0052f26913fd924f056cd15
then theto
field is0x433e064c42e87325fb6ffa9575a34862e0052f26
and thedata
field is0x3ef8e69a913fd924f056cd15000000000000000000000000000000000000000000000000
)
Rationale
The contract interface and address format combination has one notable drawback, which was brought up in discussion. This ERC can only handle deposits for native value (ETH) and not other protocols such as ERC-20. However, this is not considered a problem, because it is best practice to logically AND key-wise separate wallets for separate currencies in any exchange/merchant application for accounting reasons and also for security reasons. Therefore, using this method for the native value currency (ETH) and another method for ERC-20 tokens etc. is acceptable. Any attempt at doing something similar for ERC-20 would require modifying the ERC itself (by adding the id data as a new input argument to the transfer method etc.) which would grow the scope of this ERC too large to manage. However, if this address format catches on, it would be trivial to add the bytes8 id to any updated protocols (though adoption might be tough due to network effects).
The 8 byte size of the id and the checksum 3 : nonce 5 ratio were decided with the following considerations:
- 24 bit checksum is better than the average 15 bit checksum of an EIP-55 address.
- 40 bit nonce allows for over 1 trillion nonces.
- 64 bit length of the id was chosen as to be long enough to support a decent checksum and plenty of nonces, but not be too long. (Staying under 256 bits makes hashing cheaper in gas costs as well.)
Backwards Compatibility
An address generated with the deposit address format will not be considered a valid address for applications that don’t support it. If the user is technical enough, they can get around lack of support by verifying the checksum themselves, creating the needed data field by hand, and manually input the data field. (assuming the wallet app allows for arbitrary data input on transactions) A tool could be hosted on github for users to get the needed 20 byte address and msg.data field from a deposit address.
Since a contract following this ERC will reject any plain value transactions, there is no risk of extracting the 20 byte address and sending to it without the calldata.
However, this is a simple format, and easy to implement, so the author of this ERC will first implement in web3.js and encourage adoption with the major wallet applications.
Test Cases
[
{
"address": "0x083d6b05729c58289eb2d6d7c1bb1228d1e3f795",
"nonce": "0xbdd769c69b",
"depositAddress": "0x083d6b05729c58289eb2d6d7c1bb1228d1e3f795bdd769c69b3b97b9"
},
{
"address": "0x433e064c42e87325fb6ffa9575a34862e0052f26",
"nonce": "0x913fd924f0",
"depositAddress": "0x433e064c42e87325fb6ffa9575a34862e0052f26913fd924f056cd15"
},
{
"address": "0xbbc6597a834ef72570bfe5bb07030877c130e4be",
"nonce": "0x2c8f5b3348",
"depositAddress": "0xbbc6597a834ef72570bfe5bb07030877c130e4be2c8f5b3348023045"
},
{
"address": "0x17627b07889cd22e9fae4c6abebb9a9ad0a904ee",
"nonce": "0xe619dbb618",
"depositAddress": "0x17627b07889cd22e9fae4c6abebb9a9ad0a904eee619dbb618732ef0"
},
{
"address": "0x492cdf7701d3ebeaab63b4c7c0e66947c3d20247",
"nonce": "0x6808043984",
"depositAddress": "0x492cdf7701d3ebeaab63b4c7c0e66947c3d202476808043984183dbe"
}
]
Implementation
A sample implementation with an example contract and address generation (in the tests) is located here:
https://github.com/junderw/deposit-contract-poc
Security Considerations
In general, contracts that implement the contract interface should forward funds received to the deposit(bytes8) function to their cold wallet account. This address SHOULD be hard coded as a constant OR take advantage of the immutable
keyword in solidity versions >=0.6.5
.
To prevent problems with deposits being sent after the parent application is shut down, a contract SHOULD have a kill switch that will revert all calls to deposit(bytes8) rather than using selfdestruct(address)
(since users who deposit will still succeed, since an external account will receive value regardless of the calldata, and essentially the self-destructed contract would become a black hole for any new deposits)
Copyright
Copyright and related rights waived via CC0.