/
Mailbox.sol
400 lines (354 loc) · 20.2 KB
/
Mailbox.sol
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {IMailbox, TxStatus} from "../interfaces/IMailbox.sol";
import {Merkle} from "../libraries/Merkle.sol";
import {PriorityQueue, PriorityOperation} from "../libraries/PriorityQueue.sol";
import {TransactionValidator} from "../libraries/TransactionValidator.sol";
import {L2Message, L2Log} from "../Storage.sol";
import {UncheckedMath} from "../../common/libraries/UncheckedMath.sol";
import {UnsafeBytes} from "../../common/libraries/UnsafeBytes.sol";
import {L2ContractHelper} from "../../common/libraries/L2ContractHelper.sol";
import {AddressAliasHelper} from "../../vendor/AddressAliasHelper.sol";
import {Base} from "./Base.sol";
import {REQUIRED_L2_GAS_PRICE_PER_PUBDATA, FAIR_L2_GAS_PRICE, L1_GAS_PER_PUBDATA_BYTE, L2_L1_LOGS_TREE_DEFAULT_LEAF_HASH, PRIORITY_OPERATION_L2_TX_TYPE, PRIORITY_EXPIRATION, MAX_NEW_FACTORY_DEPS} from "../Config.sol";
import {L2_BOOTLOADER_ADDRESS, L2_TO_L1_MESSENGER_SYSTEM_CONTRACT_ADDR, L2_ETH_TOKEN_SYSTEM_CONTRACT_ADDR} from "../../common/L2ContractAddresses.sol";
/// @title zkSync Mailbox contract providing interfaces for L1 <-> L2 interaction.
/// @author Matter Labs
/// @custom:security-contact security@matterlabs.dev
contract MailboxFacet is Base, IMailbox {
using UncheckedMath for uint256;
using PriorityQueue for PriorityQueue.Queue;
string public constant override getName = "MailboxFacet";
/// @notice Prove that a specific arbitrary-length message was sent in a specific L2 batch number
/// @param _batchNumber The executed L2 batch number in which the message appeared
/// @param _index The position in the L2 logs Merkle tree of the l2Log that was sent with the message
/// @param _message Information about the sent message: sender address, the message itself, tx index in the L2 batch where the message was sent
/// @param _proof Merkle proof for inclusion of L2 log that was sent with the message
/// @return Whether the proof is valid
function proveL2MessageInclusion(
uint256 _batchNumber,
uint256 _index,
L2Message memory _message,
bytes32[] calldata _proof
) public view returns (bool) {
return _proveL2LogInclusion(_batchNumber, _index, _L2MessageToLog(_message), _proof);
}
/// @notice Prove that a specific L2 log was sent in a specific L2 batch
/// @param _batchNumber The executed L2 batch number in which the log appeared
/// @param _index The position of the l2log in the L2 logs Merkle tree
/// @param _log Information about the sent log
/// @param _proof Merkle proof for inclusion of the L2 log
/// @return Whether the proof is correct and L2 log is included in batch
function proveL2LogInclusion(
uint256 _batchNumber,
uint256 _index,
L2Log memory _log,
bytes32[] calldata _proof
) external view returns (bool) {
return _proveL2LogInclusion(_batchNumber, _index, _log, _proof);
}
/// @notice Prove that the L1 -> L2 transaction was processed with the specified status.
/// @param _l2TxHash The L2 canonical transaction hash
/// @param _l2BatchNumber The L2 batch number where the transaction was processed
/// @param _l2MessageIndex The position in the L2 logs Merkle tree of the l2Log that was sent with the message
/// @param _l2TxNumberInBatch The L2 transaction number in the batch, in which the log was sent
/// @param _merkleProof The Merkle proof of the processing L1 -> L2 transaction
/// @param _status The execution status of the L1 -> L2 transaction (true - success & 0 - fail)
/// @return Whether the proof is correct and the transaction was actually executed with provided status
/// NOTE: It may return `false` for incorrect proof, but it doesn't mean that the L1 -> L2 transaction has an opposite status!
function proveL1ToL2TransactionStatus(
bytes32 _l2TxHash,
uint256 _l2BatchNumber,
uint256 _l2MessageIndex,
uint16 _l2TxNumberInBatch,
bytes32[] calldata _merkleProof,
TxStatus _status
) public view override returns (bool) {
// Bootloader sends an L2 -> L1 log only after processing the L1 -> L2 transaction.
// Thus, we can verify that the L1 -> L2 transaction was included in the L2 batch with specified status.
//
// The semantics of such L2 -> L1 log is always:
// - sender = L2_BOOTLOADER_ADDRESS
// - key = hash(L1ToL2Transaction)
// - value = status of the processing transaction (1 - success & 0 - fail)
// - isService = true (just a conventional value)
// - l2ShardId = 0 (means that L1 -> L2 transaction was processed in a rollup shard, other shards are not available yet anyway)
// - txNumberInBatch = number of transaction in the batch
L2Log memory l2Log = L2Log({
l2ShardId: 0,
isService: true,
txNumberInBatch: _l2TxNumberInBatch,
sender: L2_BOOTLOADER_ADDRESS,
key: _l2TxHash,
value: bytes32(uint256(_status))
});
return _proveL2LogInclusion(_l2BatchNumber, _l2MessageIndex, l2Log, _merkleProof);
}
/// @notice Transfer ether from the contract to the receiver
/// @dev Reverts only if the transfer call failed
function _withdrawFunds(address _to, uint256 _amount) internal {
bool callSuccess;
// Low-level assembly call, to avoid any memory copying (save gas)
assembly {
callSuccess := call(gas(), _to, _amount, 0, 0, 0, 0)
}
require(callSuccess, "pz");
}
/// @dev Prove that a specific L2 log was sent in a specific L2 batch number
function _proveL2LogInclusion(
uint256 _batchNumber,
uint256 _index,
L2Log memory _log,
bytes32[] calldata _proof
) internal view returns (bool) {
require(_batchNumber <= s.totalBatchesExecuted, "xx");
bytes32 hashedLog = keccak256(
abi.encodePacked(_log.l2ShardId, _log.isService, _log.txNumberInBatch, _log.sender, _log.key, _log.value)
);
// Check that hashed log is not the default one,
// otherwise it means that the value is out of range of sent L2 -> L1 logs
require(hashedLog != L2_L1_LOGS_TREE_DEFAULT_LEAF_HASH, "tw");
// It is ok to not check length of `_proof` array, as length
// of leaf preimage (which is `L2_TO_L1_LOG_SERIALIZE_SIZE`) is not
// equal to the length of other nodes preimages (which are `2 * 32`)
bytes32 calculatedRootHash = Merkle.calculateRoot(_proof, _index, hashedLog);
bytes32 actualRootHash = s.l2LogsRootHashes[_batchNumber];
return actualRootHash == calculatedRootHash;
}
/// @dev Convert arbitrary-length message to the raw l2 log
function _L2MessageToLog(L2Message memory _message) internal pure returns (L2Log memory) {
return
L2Log({
l2ShardId: 0,
isService: true,
txNumberInBatch: _message.txNumberInBatch,
sender: L2_TO_L1_MESSENGER_SYSTEM_CONTRACT_ADDR,
key: bytes32(uint256(uint160(_message.sender))),
value: keccak256(_message.data)
});
}
/// @notice Estimates the cost in Ether of requesting execution of an L2 transaction from L1
/// @param _gasPrice expected L1 gas price at which the user requests the transaction execution
/// @param _l2GasLimit Maximum amount of L2 gas that transaction can consume during execution on L2
/// @param _l2GasPerPubdataByteLimit The maximum amount of L2 gas that the operator may charge the user for a single byte of pubdata.
/// @return The estimated ETH spent on L2 gas for the transaction
function l2TransactionBaseCost(
uint256 _gasPrice,
uint256 _l2GasLimit,
uint256 _l2GasPerPubdataByteLimit
) public pure returns (uint256) {
uint256 l2GasPrice = _deriveL2GasPrice(_gasPrice, _l2GasPerPubdataByteLimit);
return l2GasPrice * _l2GasLimit;
}
/// @notice Derives the price for L2 gas in ETH to be paid.
/// @param _l1GasPrice The gas price on L1.
/// @param _gasPricePerPubdata The price for each pubdata byte in L2 gas
/// @return The price of L2 gas in ETH
function _deriveL2GasPrice(uint256 _l1GasPrice, uint256 _gasPricePerPubdata) internal pure returns (uint256) {
uint256 pubdataPriceETH = L1_GAS_PER_PUBDATA_BYTE * _l1GasPrice;
uint256 minL2GasPriceETH = (pubdataPriceETH + _gasPricePerPubdata - 1) / _gasPricePerPubdata;
return Math.max(FAIR_L2_GAS_PRICE, minL2GasPriceETH);
}
/// @notice Finalize the withdrawal and release funds
/// @param _l2BatchNumber The L2 batch number where the withdrawal was processed
/// @param _l2MessageIndex The position in the L2 logs Merkle tree of the l2Log that was sent with the message
/// @param _l2TxNumberInBatch The L2 transaction number in a batch, in which the log was sent
/// @param _message The L2 withdraw data, stored in an L2 -> L1 message
/// @param _merkleProof The Merkle proof of the inclusion L2 -> L1 message about withdrawal initialization
function finalizeEthWithdrawal(
uint256 _l2BatchNumber,
uint256 _l2MessageIndex,
uint16 _l2TxNumberInBatch,
bytes calldata _message,
bytes32[] calldata _merkleProof
) external override nonReentrant {
require(!s.isEthWithdrawalFinalized[_l2BatchNumber][_l2MessageIndex], "jj");
L2Message memory l2ToL1Message = L2Message({
txNumberInBatch: _l2TxNumberInBatch,
sender: L2_ETH_TOKEN_SYSTEM_CONTRACT_ADDR,
data: _message
});
(address _l1WithdrawReceiver, uint256 _amount) = _parseL2WithdrawalMessage(_message);
bool proofValid = proveL2MessageInclusion(_l2BatchNumber, _l2MessageIndex, l2ToL1Message, _merkleProof);
require(proofValid, "pi"); // Failed to verify that withdrawal was actually initialized on L2
s.isEthWithdrawalFinalized[_l2BatchNumber][_l2MessageIndex] = true;
_withdrawFunds(_l1WithdrawReceiver, _amount);
emit EthWithdrawalFinalized(_l1WithdrawReceiver, _amount);
}
/// @notice Request execution of L2 transaction from L1.
/// @param _contractL2 The L2 receiver address
/// @param _l2Value `msg.value` of L2 transaction
/// @param _calldata The input of the L2 transaction
/// @param _l2GasLimit Maximum amount of L2 gas that transaction can consume during execution on L2
/// @param _l2GasPerPubdataByteLimit The maximum amount L2 gas that the operator may charge the user for single byte of pubdata.
/// @param _factoryDeps An array of L2 bytecodes that will be marked as known on L2
/// @param _refundRecipient The address on L2 that will receive the refund for the transaction.
/// @dev If the L2 deposit finalization transaction fails, the `_refundRecipient` will receive the `_l2Value`.
/// Please note, the contract may change the refund recipient's address to eliminate sending funds to addresses out of control.
/// - If `_refundRecipient` is a contract on L1, the refund will be sent to the aliased `_refundRecipient`.
/// - If `_refundRecipient` is set to `address(0)` and the sender has NO deployed bytecode on L1, the refund will be sent to the `msg.sender` address.
/// - If `_refundRecipient` is set to `address(0)` and the sender has deployed bytecode on L1, the refund will be sent to the aliased `msg.sender` address.
/// @dev The address aliasing of L1 contracts as refund recipient on L2 is necessary to guarantee that the funds are controllable,
/// since address aliasing to the from address for the L2 tx will be applied if the L1 `msg.sender` is a contract.
/// Without address aliasing for L1 contracts as refund recipients they would not be able to make proper L2 tx requests
/// through the Mailbox to use or withdraw the funds from L2, and the funds would be lost.
/// @return canonicalTxHash The hash of the requested L2 transaction. This hash can be used to follow the transaction status
function requestL2Transaction(
address _contractL2,
uint256 _l2Value,
bytes calldata _calldata,
uint256 _l2GasLimit,
uint256 _l2GasPerPubdataByteLimit,
bytes[] calldata _factoryDeps,
address _refundRecipient
) external payable nonReentrant returns (bytes32 canonicalTxHash) {
// Change the sender address if it is a smart contract to prevent address collision between L1 and L2.
// Please note, currently zkSync address derivation is different from Ethereum one, but it may be changed in the future.
address sender = msg.sender;
if (sender != tx.origin) {
sender = AddressAliasHelper.applyL1ToL2Alias(msg.sender);
}
// Enforcing that `_l2GasPerPubdataByteLimit` equals to a certain constant number. This is needed
// to ensure that users do not get used to using "exotic" numbers for _l2GasPerPubdataByteLimit, e.g. 1-2, etc.
// VERY IMPORTANT: nobody should rely on this constant to be fixed and every contract should give their users the ability to provide the
// ability to provide `_l2GasPerPubdataByteLimit` for each independent transaction.
// CHANGING THIS CONSTANT SHOULD BE A CLIENT-SIDE CHANGE.
require(_l2GasPerPubdataByteLimit == REQUIRED_L2_GAS_PRICE_PER_PUBDATA, "qp");
canonicalTxHash = _requestL2Transaction(
sender,
_contractL2,
_l2Value,
_calldata,
_l2GasLimit,
_l2GasPerPubdataByteLimit,
_factoryDeps,
false,
_refundRecipient
);
}
function _requestL2Transaction(
address _sender,
address _contractAddressL2,
uint256 _l2Value,
bytes calldata _calldata,
uint256 _l2GasLimit,
uint256 _l2GasPerPubdataByteLimit,
bytes[] calldata _factoryDeps,
bool _isFree,
address _refundRecipient
) internal returns (bytes32 canonicalTxHash) {
require(_factoryDeps.length <= MAX_NEW_FACTORY_DEPS, "uj");
uint64 expirationTimestamp = uint64(block.timestamp + PRIORITY_EXPIRATION); // Safe to cast
uint256 txId = s.priorityQueue.getTotalPriorityTxs();
// Here we manually assign fields for the struct to prevent "stack too deep" error
WritePriorityOpParams memory params;
// Checking that the user provided enough ether to pay for the transaction.
// Using a new scope to prevent "stack too deep" error
{
params.l2GasPrice = _isFree ? 0 : _deriveL2GasPrice(tx.gasprice, _l2GasPerPubdataByteLimit);
uint256 baseCost = params.l2GasPrice * _l2GasLimit;
require(msg.value >= baseCost + _l2Value, "mv"); // The `msg.value` doesn't cover the transaction cost
}
// If the `_refundRecipient` is not provided, we use the `_sender` as the recipient.
address refundRecipient = _refundRecipient == address(0) ? _sender : _refundRecipient;
// If the `_refundRecipient` is a smart contract, we apply the L1 to L2 alias to prevent foot guns.
if (refundRecipient.code.length > 0) {
refundRecipient = AddressAliasHelper.applyL1ToL2Alias(refundRecipient);
}
params.sender = _sender;
params.txId = txId;
params.l2Value = _l2Value;
params.contractAddressL2 = _contractAddressL2;
params.expirationTimestamp = expirationTimestamp;
params.l2GasLimit = _l2GasLimit;
params.l2GasPricePerPubdata = _l2GasPerPubdataByteLimit;
params.valueToMint = msg.value;
params.refundRecipient = refundRecipient;
canonicalTxHash = _writePriorityOp(params, _calldata, _factoryDeps);
}
function _serializeL2Transaction(
WritePriorityOpParams memory _priorityOpParams,
bytes calldata _calldata,
bytes[] calldata _factoryDeps
) internal pure returns (L2CanonicalTransaction memory transaction) {
transaction = L2CanonicalTransaction({
txType: PRIORITY_OPERATION_L2_TX_TYPE,
from: uint256(uint160(_priorityOpParams.sender)),
to: uint256(uint160(_priorityOpParams.contractAddressL2)),
gasLimit: _priorityOpParams.l2GasLimit,
gasPerPubdataByteLimit: _priorityOpParams.l2GasPricePerPubdata,
maxFeePerGas: uint256(_priorityOpParams.l2GasPrice),
maxPriorityFeePerGas: uint256(0),
paymaster: uint256(0),
// Note, that the priority operation id is used as "nonce" for L1->L2 transactions
nonce: uint256(_priorityOpParams.txId),
value: _priorityOpParams.l2Value,
reserved: [_priorityOpParams.valueToMint, uint256(uint160(_priorityOpParams.refundRecipient)), 0, 0],
data: _calldata,
signature: new bytes(0),
factoryDeps: _hashFactoryDeps(_factoryDeps),
paymasterInput: new bytes(0),
reservedDynamic: new bytes(0)
});
}
/// @notice Stores a transaction record in storage & send event about that
function _writePriorityOp(
WritePriorityOpParams memory _priorityOpParams,
bytes calldata _calldata,
bytes[] calldata _factoryDeps
) internal returns (bytes32 canonicalTxHash) {
L2CanonicalTransaction memory transaction = _serializeL2Transaction(_priorityOpParams, _calldata, _factoryDeps);
bytes memory transactionEncoding = abi.encode(transaction);
TransactionValidator.validateL1ToL2Transaction(transaction, transactionEncoding, s.priorityTxMaxGasLimit);
canonicalTxHash = keccak256(transactionEncoding);
s.priorityQueue.pushBack(
PriorityOperation({
canonicalTxHash: canonicalTxHash,
expirationTimestamp: _priorityOpParams.expirationTimestamp,
layer2Tip: uint192(0) // TODO: Restore after fee modeling will be stable. (SMA-1230)
})
);
// Data that is needed for the operator to simulate priority queue offchain
emit NewPriorityRequest(
_priorityOpParams.txId,
canonicalTxHash,
_priorityOpParams.expirationTimestamp,
transaction,
_factoryDeps
);
}
/// @notice Hashes the L2 bytecodes and returns them in the format in which they are processed by the bootloader
function _hashFactoryDeps(
bytes[] calldata _factoryDeps
) internal pure returns (uint256[] memory hashedFactoryDeps) {
uint256 factoryDepsLen = _factoryDeps.length;
hashedFactoryDeps = new uint256[](factoryDepsLen);
for (uint256 i = 0; i < factoryDepsLen; i = i.uncheckedInc()) {
bytes32 hashedBytecode = L2ContractHelper.hashL2Bytecode(_factoryDeps[i]);
// Store the resulting hash sequentially in bytes.
assembly {
mstore(add(hashedFactoryDeps, mul(add(i, 1), 32)), hashedBytecode)
}
}
}
/// @dev Decode the withdraw message that came from L2
function _parseL2WithdrawalMessage(
bytes memory _message
) internal pure returns (address l1Receiver, uint256 amount) {
// We check that the message is long enough to read the data.
// Please note that there are two versions of the message:
// 1. The message that is sent by `withdraw(address _l1Receiver)`
// It should be equal to the length of the bytes4 function signature + address l1Receiver + uint256 amount = 4 + 20 + 32 = 56 (bytes).
// 2. The message that is sent by `withdrawWithMessage(address _l1Receiver, bytes calldata _additionalData)`
// It should be equal to the length of the following:
// bytes4 function signature + address l1Receiver + uint256 amount + address l2Sender + bytes _additionalData =
// = 4 + 20 + 32 + 32 + _additionalData.length >= 68 (bytes).
// So the data is expected to be at least 56 bytes long.
require(_message.length >= 56, "pm");
(uint32 functionSignature, uint256 offset) = UnsafeBytes.readUint32(_message, 0);
require(bytes4(functionSignature) == this.finalizeEthWithdrawal.selector, "is");
(l1Receiver, offset) = UnsafeBytes.readAddress(_message, offset);
(amount, offset) = UnsafeBytes.readUint256(_message, offset);
}
}