Summary:
Right now, only ETH is supported for hyperchain's base token. There are several use cases where a hyperchain would like to have a specific erc20 token as a base token rather than the native ETH.
In this issue, we are proposing to modify the "deposit" and "withdrawal" logic in the zksync contracts so that it could handle custom base token.
Problem definition
To understand the problem, we need to describe how the base token are currently "minted" and "burned" in layer 2.
The balance of the base token for all accounts is maintained by the L2EthToken.sol contract on L2.
The mint and burn can only be called by the bootloader.
Mint
The current "mint" mechanism is controlled by an event emitted by the zksync's diamond contract on L1
event NewPriorityRequest(
uint256 txId,
bytes32 txHash,
uint64 expirationTimestamp,
L2CanonicalTransaction transaction,
bytes[] factoryDeps
);
With
struct L2CanonicalTransaction {
uint256 txType;
uint256 from;
uint256 to;
uint256 gasLimit;
uint256 gasPerPubdataByteLimit;
uint256 maxFeePerGas;
uint256 maxPriorityFeePerGas;
uint256 paymaster;
uint256 nonce;
uint256 value;
// In the future, we might want to add some
// new fields to the struct. The `txData` struct
// is to be passed to account and any changes to its structure
// would mean a breaking change to these accounts. To prevent this,
// we should keep some fields as "reserved".
// It is also recommended that their length is fixed, since
// it would allow easier proof integration (in case we will need
// some special circuit for preprocessing transactions).
uint256[4] reserved;
bytes data;
bytes signature;
uint256[] factoryDeps;
bytes paymasterInput;
// Reserved dynamic type for the future use-case. Using it should be avoided,
// But it is still here, just in case we want to enable some additional functionality.
bytes reservedDynamic;
}
The structure L2CanonicalTransaction contains a reserved
field in which the first 32bytes are allocated to the valueToMint (let's call it reserved0 field)
A watcher monitors the Ethereum events emitted from the zksyncβs diamond contract and creates a transaction on layer2 for each of this event recorded.
The bootloader, upon processing the transaction, will check the the value specified in the reserved0 field. In case this value is not null, will mint the specified value to the specified destination address.
Some safety checks are implemented to ensure that no value is created from thin air
- The reserved0 field(value to mint) specified in the event is equal to the transaction
msg.value
. The zksync contract is used to store all the bridged ETH.
Total ETH mint = Total ETH stored in zksync contract account
- To reduce the chance of failure, we also ensure that this value is enough to cover the transaction (cost & value) on L2.
Burn
The "burn" mechanism is controlled by the L2EthToken.sol contract on L2. By calling the withdrawal method, the amount specified in the transaction msg.value
is burned from the sender account and a message is inserted to the L1_MESSENGER_CONTRACT
. The message contains the withdrawal amount and the receiver address.
The eth_sender picks the message, includes it in a L2 transaction, and then encapsulates it in a block.
Upon executing the block on L1, the finalizeEthWithdrawal method from the zksync contract can be called on the block and process any messages it contains.
The ETH will be released to the specified receiver when processing the withdrawal message.
Proposed solution:
We follow the principle on "minimum change"
In this solution, 1 unit
of token in L1 (locked) correspond to 1 unit
of base token in L2 . The token contract on L1 should follow the ERC20 standard and have 18 decimals.
The only part that requires change in the solution is the "lock" and "unlock" mechanism on the L1 smart contract (mailbox) and the field βvalueToMintβ be redefined.
The logic of βmintβ and βburnβ on layer2 remains the same, however it is crucial to implement an Oracle in the future to adjust the gas_price on layer2 according to the related price of the base token against Ethereum (could be a coefficient factor stored in DB and updated by a trusted oracle) as well a mechanism for the operator to convert the base token to ETH on L1 so that it can cover its costs.
We will only describe the changes needed in the smart contract in this issue.
The idea is to introduce a new "lock" and "unlock" mechanism at the Mailbox facet level and that instead of setting the valueToMint
based on the msg.value, will use the value corresponding to the amount of ERC20 locked(safeTransfer) to the contract.
- The msg.value is ignored and instead we will introduce a new input parameter
uint256 _baseAmount
. User can use this new parameter to specify the amount of token to be bridged.
https://github.com/matter-labs/era-contracts/pull/12/files#diff-df9164d8de562b3612626c482a4d9f1c014d796f1bd4d5dd42a210386a5bcf6dR263
- In case of mint event, we have to explicitly transfer the ERC20 from the sender address to the contract address (lock)
https://github.com/matter-labs/era-contracts/pull/12/files#diff-df9164d8de562b3612626c482a4d9f1c014d796f1bd4d5dd42a210386a5bcf6dR245
- In case of withdrawal event, we have to explicitly transfer the ERC20 from the contract address to the sender address
https://github.com/matter-labs/era-contracts/pull/12/files#diff-df9164d8de562b3612626c482a4d9f1c014d796f1bd4d5dd42a210386a5bcf6dR110
- The logic to decide if we want to use an ERC20 or ETH as base token is defined by the
baseTokenAddress
value stored by the diamond proxy contract during initialization. This value represents the address of the ERC20 token to be used as base token. If it is zero, then we fallback to the default behavior which is to use ETH as base token
https://github.com/matter-labs/era-contracts/pull/12/files#diff-8c8f228bb83a30dbd6f369160e2016a869e01b68b1c106fc39196742f361d33bR50
- The logic on
L1WethBridge
can be completely ignored. WETH can be treated as any ERC20 token
Points to be discussed
- Should be make the changes in the existing contract or implement in new facets ?
It is possible to introduce a new facet CustomBaseMailBox
and implement logic related to custom base token there instead of modifying the existing MailBox
facet.
The script would need to be modified to deploy either CustomBaseMailBox
or MailBox
depending on the deployer use cases.
Pro : Non breaking change
Cons : More facets to maintains. Each changes in one facet would need to be duplicated in the other facet