/
L2MessageService.sol
254 lines (210 loc) · 8 KB
/
L2MessageService.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
// SPDX-License-Identifier: AGPL-3.0
pragma solidity 0.8.19;
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import { IMessageService } from "../../interfaces/IMessageService.sol";
import { IGenericErrors } from "../../interfaces/IGenericErrors.sol";
import { RateLimiter } from "../lib/RateLimiter.sol";
import { L2MessageManager } from "./L2MessageManager.sol";
/**
* @title Contract to manage cross-chain messaging on L2.
* @author ConsenSys Software Inc.
* @custom:security-contact security-report@linea.build
*/
contract L2MessageService is
Initializable,
RateLimiter,
L2MessageManager,
ReentrancyGuardUpgradeable,
IMessageService,
IGenericErrors
{
// Keep free storage slots for future implementation updates to avoid storage collision.
// @dev NB: Take note that this is at the beginning of the file where other storage gaps,
// are at the end of files. Be careful with how storage is adjusted on upgrades.
uint256[50] private __gap_L2MessageService;
bytes32 public constant MINIMUM_FEE_SETTER_ROLE = keccak256("MINIMUM_FEE_SETTER_ROLE");
address private _messageSender;
// @dev initialise to save user cost with existing slot.
uint256 public nextMessageNumber;
// @dev initialise minimumFeeInWei variable.
uint256 public minimumFeeInWei;
// @dev adding these should not affect storage as they are constants and are stored in bytecode.
uint256 private constant REFUND_OVERHEAD_IN_GAS = 47500;
address private constant DEFAULT_SENDER_ADDRESS = address(123456789);
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
/**
* @notice Initialises underlying message service dependencies.
* @param _securityCouncil The address owning the security council role.
* @param _l1l2MessageSetter The address owning the add L1L2MessageHashes functionality.
* @param _rateLimitPeriod The period to rate limit against.
* @param _rateLimitAmount The limit allowed for withdrawing the period.
*/
function initialize(
address _securityCouncil,
address _l1l2MessageSetter,
uint256 _rateLimitPeriod,
uint256 _rateLimitAmount
) public initializer {
if (_securityCouncil == address(0)) {
revert ZeroAddressNotAllowed();
}
if (_l1l2MessageSetter == address(0)) {
revert ZeroAddressNotAllowed();
}
__ERC165_init();
__Context_init();
__AccessControl_init();
__RateLimiter_init(_rateLimitPeriod, _rateLimitAmount);
__L2MessageManager_init(_l1l2MessageSetter);
nextMessageNumber = 1;
_grantRole(DEFAULT_ADMIN_ROLE, _securityCouncil);
_grantRole(MINIMUM_FEE_SETTER_ROLE, _securityCouncil);
_grantRole(RATE_LIMIT_SETTER_ROLE, _securityCouncil);
_grantRole(PAUSE_MANAGER_ROLE, _securityCouncil);
_messageSender = DEFAULT_SENDER_ADDRESS;
}
/**
* @notice Adds a message for sending cross-chain and emits a relevant event.
* @dev The message number is preset and only incremented at the end if successful for the next caller.
* @param _to The address the message is intended for.
* @param _fee The fee being paid for the message delivery.
* @param _calldata The calldata to pass to the recipient.
*/
function sendMessage(address _to, uint256 _fee, bytes calldata _calldata) external payable {
_requireTypeNotPaused(L2_L1_PAUSE_TYPE);
_requireTypeNotPaused(GENERAL_PAUSE_TYPE);
if (_to == address(0)) {
revert ZeroAddressNotAllowed();
}
if (_fee > msg.value) {
revert ValueSentTooLow();
}
uint256 coinbaseFee = minimumFeeInWei;
if (_fee < coinbaseFee) {
revert FeeTooLow();
}
uint256 postmanFee;
uint256 valueSent;
unchecked {
postmanFee = _fee - coinbaseFee;
valueSent = msg.value - _fee;
}
uint256 messageNumber = nextMessageNumber;
/// @dev Rate limit and revert is in the rate limiter.
_addUsedAmount(valueSent + postmanFee);
bytes32 messageHash = keccak256(abi.encode(msg.sender, _to, postmanFee, valueSent, messageNumber, _calldata));
nextMessageNumber++;
(bool success, ) = block.coinbase.call{ value: coinbaseFee }("");
if (!success) {
revert FeePaymentFailed(block.coinbase);
}
emit MessageSent(msg.sender, _to, postmanFee, valueSent, messageNumber, _calldata, messageHash);
}
/**
* @notice Claims and delivers a cross-chain message.
* @dev _feeRecipient Can be set to address(0) to receive as msg.sender.
* @dev messageSender Is set temporarily when claiming and reset post.
* @param _from The address of the original sender.
* @param _to The address the message is intended for.
* @param _fee The fee being paid for the message delivery.
* @param _value The value to be transferred to the destination address.
* @param _feeRecipient The recipient for the fee.
* @param _calldata The calldata to pass to the recipient.
* @param _nonce The unique auto generated message number used when sending the message.
*/
function claimMessage(
address _from,
address _to,
uint256 _fee,
uint256 _value,
address payable _feeRecipient,
bytes calldata _calldata,
uint256 _nonce
) external nonReentrant distributeFees(_fee, _to, _calldata, _feeRecipient) {
_requireTypeNotPaused(L1_L2_PAUSE_TYPE);
_requireTypeNotPaused(GENERAL_PAUSE_TYPE);
bytes32 messageHash = keccak256(abi.encode(_from, _to, _fee, _value, _nonce, _calldata));
/// @dev Status check and revert is in the message manager.
_updateL1L2MessageStatusToClaimed(messageHash);
_messageSender = _from;
(bool callSuccess, bytes memory returnData) = _to.call{ value: _value }(_calldata);
if (!callSuccess) {
if (returnData.length > 0) {
assembly {
let data_size := mload(returnData)
revert(add(32, returnData), data_size)
}
} else {
revert MessageSendingFailed(_to);
}
}
_messageSender = DEFAULT_SENDER_ADDRESS;
emit MessageClaimed(messageHash);
}
/**
* @notice The Fee Manager sets a minimum fee to address DOS protection.
* @param _feeInWei New minimum fee in Wei.
*/
function setMinimumFee(uint256 _feeInWei) external onlyRole(MINIMUM_FEE_SETTER_ROLE) {
minimumFeeInWei = _feeInWei;
}
/**
* @dev The _messageSender address is set temporarily when claiming.
* @return _messageSender address.
*/
function sender() external view returns (address) {
return _messageSender;
}
/**
* @notice Function to receive funds for liquidity purposes.
*/
receive() external payable virtual {}
/**
* @notice The unspent fee is refunded if applicable.
* @param _feeInWei The fee paid for delivery in Wei.
* @param _to The recipient of the message and gas refund.
* @param _calldata The calldata of the message.
*/
modifier distributeFees(
uint256 _feeInWei,
address _to,
bytes calldata _calldata,
address _feeRecipient
) {
//pre-execution
uint256 startingGas = gasleft();
_;
//post-execution
// we have a fee
if (_feeInWei > 0) {
// default postman fee
uint256 deliveryFee = _feeInWei;
// do we have empty calldata?
if (_calldata.length == 0) {
bool isDestinationEOA;
assembly {
isDestinationEOA := iszero(extcodesize(_to))
}
// are we calling an EOA
if (isDestinationEOA) {
// initial + cost to call and refund minus gasleft
deliveryFee = (startingGas + REFUND_OVERHEAD_IN_GAS - gasleft()) * tx.gasprice;
if (_feeInWei > deliveryFee) {
payable(_to).send(_feeInWei - deliveryFee);
} else {
deliveryFee = _feeInWei;
}
}
}
address feeReceiver = _feeRecipient == address(0) ? msg.sender : _feeRecipient;
bool callSuccess = payable(feeReceiver).send(deliveryFee);
if (!callSuccess) {
revert FeePaymentFailed(feeReceiver);
}
}
}
}