While Frontier provides a translation scheme for
H256 → H160
(simply drop the last 96 bits LE), there’s no deterministic way to convertH160 → H256
at execution time. So when some incoming RPC carries anH160
address as argument, how do we convert that intoH256
on the fly?
Not quite right, Frontier provides translation to the other side: H160 -> H256
, trait AddressMapping
.
The default implementation for the Substrate is HashedAddressMapping
: hash("evm:" ++ address)
.
This mapping determines which Substrate address should hold the Ethereum account balance and nonce.
H256 -> H160
mapping (trait EnsureAddressOrigin
) is not exposed directly; there is only a trait provided to check if H160
address provided by the user is allowed to be used for evm_call
/evm_withdraw
for the specified H256
.
The default implementation for the Substrate is EnsureAddressTruncated
: origin[0..20] == address
.
While it is possible to create on-chain mapping between H160 and H256 addresses, this approach is not perfect, as it requires some form of registration.
This registration procedure may break some Ethereum RPC compatibility, i.e. Acala requires a custom web3.js provider to work.
Without such a form of compatibility layer, you can’t reliably share any resources between substrate and Ethereum accounts.
As the Ethereum account linking may be added at an arbitrary time, and you can’t know the mapped address ahead of time, you still can’t use a single address for everything.
Example:
- Alice has linked Ethereum address Alith on chain A.
- Someone transfers an NFT to Alith on the chain B. Chain B doesn’t yet know which substrate address Alith will have, so it still needs to have the original Ethereum address (Alith) stored.
- When Alice registers Alith’s mapping on chain B, assets should be either transferred to Alice (And this may not be possible if there is a large number of assets) or kept on Alith address (Then why do we need the mapping in the first place?).
At Unique, we have embraced the asymmetrical mapping for Ethereum (hashing when converting H160 -> H256
and truncation when converting H256 -> H160
(TruncateAddress
)).
There were a couple of challenges to solve, but in the end, we believe that our solution is pretty good for the general usage.
For cases when we need to return the resource owner to the Ethereum side, e.g NFT ownerOf
, we keep an original user address in enum CrossAccount {Substrate(AccountId), Ethereum(H160)}
: https://github.com/UniqueNetwork/unique-frontier/blob/b1ff36163b15a36c1f73950c722cb1f3939d287d/frame/evm/src/account.rs#L53
What problem does it solve?
- Let Alice be the Substrate account.
- Let Alith be
TruncateAddress(Alice)
. - We mint NFT to Alith’s account.
- If we only store the substrate address as an NFT owner, then the resulting NFT owner will be, say, Alyssa =
HashedAddressMapping(TruncateAddress(Alice))
. - And then,
ownerOf
will returnTruncateAddress(Alyssa)
, which is not the same asTruncateAddress(Alice)
, thus the result ofownerOf
is useless.
Instead, we store CrossAccount::Ethereum(Alith)
, so ownerOf
will return the correct address: Alith.
The same applies when the token transfer target is Alice. In this case, we store CrossAccount::Substrate(Alice)
, and ownerOf
will return TruncateAddress(Alice)
, which is precisely Alith.
The other problem, which arises when no bidirectional translation is available, is Ethereum transaction fees/payment. Our solution is to delay any address conversions and use CrossAccountId
in Frontier everywhere it is possible: feat: move address conversions to the lower level · UniqueNetwork/unique-frontier@f814b3f · GitHub
This way, when the user tries to call an Ethereum contract using pallet_evm.call
method, payment (Both fee and value) is done from the substrate address, instead of using HashedAddressMapping(TruncateAddress(origin))
, as in vanilla frontier.