This repository has been archived by the owner on Mar 20, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 18
/
L1_NovaExecutionManager.sol
326 lines (271 loc) · 16.7 KB
/
L1_NovaExecutionManager.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
// @unsupported: ovm
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.7.6;
pragma abicoder v2;
import {Auth} from "@rari-capital/solmate/src/auth/Auth.sol";
import {SafeMath} from "@openzeppelin/contracts/math/SafeMath.sol";
import {NovaExecHashLib} from "./libraries/NovaExecHashLib.sol";
import {CrossDomainEnabled, iOVM_CrossDomainMessenger} from "./external/CrossDomainEnabled.sol";
import {L2_NovaRegistry} from "./L2_NovaRegistry.sol";
import {L1_NovaApprovalEscrow} from "./L1_NovaApprovalEscrow.sol";
/// @notice Entry point for relayers to execute requests.
/// @dev Deploys an L1_NovaApprovalEscrow and sends cross domain messages to the L2_NovaRegistry.
contract L1_NovaExecutionManager is Auth, CrossDomainEnabled {
using SafeMath for uint256;
/*///////////////////////////////////////////////////////////////
CONSTANTS
//////////////////////////////////////////////////////////////*/
/// @notice The revert message text used to trigger a hard revert.
/// @notice The execution manager will ignore hard reverts if they are triggered by a strategy not registered as UNSAFE.
string public constant HARD_REVERT_TEXT = "__NOVA__HARD__REVERT__";
/// @notice The keccak256 hash of the hard revert text.
/// @dev The exec function uses this hash the compare the revert reason of an execution with the hard revert text.
bytes32 public constant HARD_REVERT_HASH = keccak256(abi.encodeWithSignature("Error(string)", HARD_REVERT_TEXT));
/// @notice The 'default' value for currentExecHash.
/// @dev Outside of an active exec call currentExecHash will always equal DEFAULT_EXECHASH.
bytes32 public constant DEFAULT_EXECHASH = 0xFEEDFACECAFEBEEFFEEDFACECAFEBEEFFEEDFACECAFEBEEFFEEDFACECAFEBEEF;
/*///////////////////////////////////////////////////////////////
IMMUTABLES
//////////////////////////////////////////////////////////////*/
/// @notice The address of the L2_NovaRegistry to send cross domain messages to.
/// @dev This address will not have contract code on L1, it is the address of a contract
/// deployed on L2. We can only communicate with this address using cross domain messages.
address public immutable L2_NOVA_REGISTRY_ADDRESS;
/// @notice The address of the L1_NovaApprovalEscrow to access tokens from.
/// @dev The transferFromRelayer function uses the escrow as a proxy identity for relayers to approve their tokens to, where
/// only the execution manager can transfer them. If relayers approved tokens directly to the execution manager, another relayer
/// could steal them by calling exec with the token set as the strategy and transferFrom or pull (used by DAI/MKR) used as calldata.
L1_NovaApprovalEscrow public immutable L1_NOVA_APPROVAL_ESCROW;
/// @param _L2_NOVA_REGISTRY_ADDRESS The address of the L2_NovaRegistry on L2 to send cross domain messages to.
/// @param _CROSS_DOMAIN_MESSENGER The L1 cross domain messenger contract to use for sending cross domain messages.
constructor(address _L2_NOVA_REGISTRY_ADDRESS, iOVM_CrossDomainMessenger _CROSS_DOMAIN_MESSENGER)
CrossDomainEnabled(_CROSS_DOMAIN_MESSENGER)
{
L2_NOVA_REGISTRY_ADDRESS = _L2_NOVA_REGISTRY_ADDRESS;
// Create an approval escrow which implicitly becomes
// owned by the execution manager in its constructor.
L1_NOVA_APPROVAL_ESCROW = new L1_NovaApprovalEscrow();
}
/*///////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
/// @notice Emitted when `updateGasConfig` is called.
/// @param newGasConfig The updated gasConfig.
event GasConfigUpdated(GasConfig newGasConfig);
/// @notice Emitted when `registerSelfAsStrategy` is called.
/// @param strategyRiskLevel The risk level the strategy registered itself as.
event StrategyRegistered(StrategyRiskLevel strategyRiskLevel);
/// @notice Emitted when `exec` is called.
/// @param execHash The execHash computed from arguments and transaction context.
/// @param reverted Will be true if the strategy call reverted, will be false if not.
/// @param gasUsed The gas estimate computed during the call.
event Exec(bytes32 indexed execHash, address relayer, bool reverted, uint256 gasUsed);
/*///////////////////////////////////////////////////////////////
GAS LIMIT/ESTIMATION CONFIGURATION
//////////////////////////////////////////////////////////////*/
/// @dev Packed struct of gas limit/estimation configuration values used in exec.
/// @param calldataByteGasEstimate The amount of gas to assume each byte of calldata consumes.
/// @param missingGasEstimate The extra amount of gas the system consumes but cannot measure on the fly.
/// @param strategyCallGasBuffer The extra amount of gas to keep as a buffer when calling a strategy.
/// @param execCompletedMessageGasLimit The L2 gas limit to use for the cross domain call to execCompleted.
struct GasConfig {
// This needs to factor in raw calldata costs, along with the hidden
// cost of abi decoding and copying the calldata into an Solidity function.
uint32 calldataByteGasEstimate;
// This needs to factor in the base transaction gas (currently 21000), along
// with the gas cost of sending the cross domain message and emitting the Exec event.
uint96 missingGasEstimate;
// This needs to factor in the max amount of gas consumed after the strategy call, up
// until the cross domain message is sent (as this is not accounted for in missingGasEstimate).
uint96 strategyCallGasBuffer;
// This needs to factor in the overhead of relaying the message on L2 (currently ~800k),
// along with the actual L2 gas cost of calling the L2_NovaRegistry's execCompleted function.
uint32 execCompletedMessageGasLimit;
}
/// @notice Gas limit/estimation configuration values used in exec.
GasConfig public gasConfig =
GasConfig({
calldataByteGasEstimate: 13, // OpenGSN uses 13 to estimate gas per calldata byte too.
missingGasEstimate: 200000, // Rough estimate for missing gas. Tune this in production.
strategyCallGasBuffer: 5000, // Overly cautious gas buffer. Can likely be safely reduced.
execCompletedMessageGasLimit: 1500000 // If the limit is too low, relayers won't get paid.
});
/// @notice Updates the gasConfig.
/// @param newGasConfig The updated value to use for gasConfig.
function updateGasConfig(GasConfig calldata newGasConfig) external requiresAuth {
gasConfig = newGasConfig;
emit GasConfigUpdated(newGasConfig);
}
/*///////////////////////////////////////////////////////////////
STRATEGY RISK LEVEL STORAGE
//////////////////////////////////////////////////////////////*/
/// @notice Risk classifications for strategies.
enum StrategyRiskLevel {
// The strategy has not been assigned a risk level.
// It has the equivalent abilities of a SAFE strategy,
// but could upgrade itself to an UNSAFE strategy at any time.
UNKNOWN,
// The strategy has registered itself as a safe strategy,
// meaning it cannot use transferFromRelayer or trigger a hard
// revert. A SAFE strategy cannot upgrade itself to become UNSAFE.
SAFE,
// The strategy has registered itself as an unsafe strategy,
// meaning it has access to all the functionality the execution
// manager provides like transferFromRelayer and the ability to hard
// revert. An UNSAFE strategy cannot downgrade itself to become SAFE.
UNSAFE
}
/// @notice Maps strategy addresses to their registered risk level.
/// @dev This mapping is used to determine if strategies can access transferFromRelayer and trigger hard reverts.
mapping(address => StrategyRiskLevel) public getStrategyRiskLevel;
/// @notice Registers the caller as a strategy with the provided risk level.
/// @dev A strategy can only register once, and will have no way to change its risk level after registering.
/// @param strategyRiskLevel The risk level the strategy is registering as. Strategies cannot register as UNKNOWN.
function registerSelfAsStrategy(StrategyRiskLevel strategyRiskLevel) external requiresAuth {
// Ensure the strategy has not already registered itself, as if strategies could change their risk level arbitrarily
// they would be able to trick relayers into executing them believing they were safe, and then use unsafe functionality.
require(getStrategyRiskLevel[msg.sender] == StrategyRiskLevel.UNKNOWN, "ALREADY_REGISTERED");
// Strategies can't register as UNKNOWN because it would emit an unhelpful StrategyRegistered event and confuse relayers.
require(strategyRiskLevel != StrategyRiskLevel.UNKNOWN, "INVALID_RISK_LEVEL");
// Set the strategy's risk level.
getStrategyRiskLevel[msg.sender] = strategyRiskLevel;
emit StrategyRegistered(strategyRiskLevel);
}
/*///////////////////////////////////////////////////////////////
EXECUTION CONTEXT STORAGE
//////////////////////////////////////////////////////////////*/
/// @notice The address who called exec.
/// @dev This will not be reset after each execution completes.
address public currentRelayer;
/// @notice The address of the strategy that is currently being called.
/// @dev This will not be reset after each execution completes.
address public currentlyExecutingStrategy;
/// @notice The execHash computed from the currently executing call to exec.
/// @dev This will be reset to DEFAULT_EXECHASH after each execution completes.
bytes32 public currentExecHash = DEFAULT_EXECHASH;
/*///////////////////////////////////////////////////////////////
EXECUTION LOGIC
//////////////////////////////////////////////////////////////*/
/// @notice Executes a request and sends tips/gas/inputs to a specific address on L2.
/// @param nonce The nonce of the request to execute.
/// @param strategy The strategy specified in the request.
/// @param l1Calldata The calldata associated with the request.
/// @param l2Recipient An address who will receive the tips, gas and input tokens attached to the request on L2.
/// @param deadline Timestamp after which the transaction will immediately revert.
function exec(
uint256 nonce,
address strategy,
bytes calldata l1Calldata,
uint256 gasLimit,
address l2Recipient,
uint256 deadline
) external {
// Measure gas left at the start of execution.
uint256 startGas = gasleft();
// Check that the deadline has not already passed.
require(block.timestamp <= deadline, "PAST_DEADLINE");
// Substitute for Auth's requiresAuth modifier.
require(isAuthorized(msg.sender, msg.sig), "UNAUTHORIZED");
// Prevent the strategy or another contract from trying
// to frontrun a relayer's execution and take their payment.
require(currentExecHash == DEFAULT_EXECHASH, "ALREADY_EXECUTING");
// We cannot allow calling cross domain messenger directly, as a
// malicious relayer could use it to trigger the registry's execCompleted
// function and claim bounties without actually executing the proper request(s).
require(strategy != address(CROSS_DOMAIN_MESSENGER), "UNSAFE_STRATEGY");
// We cannot allow calling the approval escrow directly, as a malicious
// relayer could call its transferTokenToStrategy function and access tokens
// from other relayers outside of a proper call to the transferFromRelayer function.
require(strategy != address(L1_NOVA_APPROVAL_ESCROW), "UNSAFE_STRATEGY");
// We cannot allow calling the execution manager itself, as any malicious
// relayer could exploit Auth inherited functions to change ownership, blacklist
// other relayers, or freeze the contract entirely, without being properly authorized.
require(strategy != address(this), "UNSAFE_STRATEGY");
// Compute the relevant execHash.
bytes32 execHash = NovaExecHashLib.compute({
nonce: nonce,
strategy: strategy,
l1Calldata: l1Calldata,
gasLimit: gasLimit,
gasPrice: tx.gasprice
});
// Initialize execution context.
currentExecHash = execHash;
currentRelayer = msg.sender;
currentlyExecutingStrategy = strategy;
// Call the strategy with a safe gas limit.
(bool success, bytes memory returnData) = strategy.call{
gas: gasLimit
.sub(msg.data.length.mul(gasConfig.calldataByteGasEstimate))
.sub(gasConfig.strategyCallGasBuffer)
.sub(gasConfig.missingGasEstimate)
.sub(startGas - gasleft())
}(l1Calldata);
// Revert if a valid hard revert was triggered. A hard revert is only valid if the strategy had a risk level of UNSAFE.
require(
success || keccak256(returnData) != HARD_REVERT_HASH || getStrategyRiskLevel[strategy] != StrategyRiskLevel.UNSAFE,
"HARD_REVERT"
);
// Reset currentExecHash to default so transferFromRelayer becomes uncallable again.
currentExecHash = DEFAULT_EXECHASH;
// Estimate how much gas this tx will have consumed in total (not accounting for refunds).
uint256 gasUsedEstimate = msg.data.length.mul(gasConfig.calldataByteGasEstimate).add(gasConfig.missingGasEstimate).add(
startGas - gasleft()
);
// Send message to unlock the bounty on L2.
CROSS_DOMAIN_MESSENGER.sendMessage(
L2_NOVA_REGISTRY_ADDRESS,
abi.encodeWithSelector(
L2_NovaRegistry.execCompleted.selector,
// Computed execHash:
execHash,
// The reward recipient on L2:
l2Recipient,
// Did the call revert:
!success,
// Estimated gas used in total:
gasUsedEstimate
),
gasConfig.execCompletedMessageGasLimit
);
emit Exec(execHash, msg.sender, !success, gasUsedEstimate);
}
/*///////////////////////////////////////////////////////////////
STRATEGY UTILITIES
//////////////////////////////////////////////////////////////*/
/// @notice Transfers tokens the relayer (the address that called exec)
/// approved to the L1_NOVA_APPROVAL_ESCROW to currently executing strategy.
/// @notice Can only be called by the currently executing strategy (if there is one at all).
/// @notice The currently execution strategy must be registered as UNSAFE to use this function.
/// @notice Will hard revert if the correct amount of tokens are not approved to the escrow.
/// @param token The ER20 token to transfer to the currently executing strategy.
/// @param amount The amount of the token to transfer to the currently executing strategy.
function transferFromRelayer(address token, uint256 amount) external requiresAuth {
// Only the currently executing strategy is allowed to call this function.
// Since msg.sender is inexpensive, from here on it's used to access the strategy.
require(msg.sender == currentlyExecutingStrategy, "NOT_CURRENT_STRATEGY");
// Ensure currentExecHash is not set to DEFAULT_EXECHASH as otherwise a
// malicious strategy could transfer tokens outside of an active execution.
require(currentExecHash != DEFAULT_EXECHASH, "NO_ACTIVE_EXECUTION");
// Ensure the strategy has registered itself as UNSAFE so relayers can
// avoid strategies that use transferFromRelayer if they want to be cautious.
require(getStrategyRiskLevel[msg.sender] == StrategyRiskLevel.UNSAFE, "UNSUPPORTED_RISK_LEVEL");
// Transfer tokens from the relayer to the strategy.
require(
L1_NOVA_APPROVAL_ESCROW.transferApprovedToken({
token: token,
amount: amount,
sender: currentRelayer,
recipient: msg.sender
}),
HARD_REVERT_TEXT // Hard revert if the transfer fails.
);
}
/// @notice Convenience function that triggers a hard revert.
/// @notice The execution manager will ignore hard reverts if
/// they are triggered by a strategy not registered as UNSAFE.
function hardRevert() external pure {
// Call revert with the hard revert text.
revert(HARD_REVERT_TEXT);
}
}