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
//! Error types for sandbox execution.
use alloy_primitives::Bytes;
use alloy_sol_types::SolError;
use mega_system_contracts::keyless_deploy::IKeylessDeploy;
use crate::{LimitKind, MegaHaltReason};
/// Error types for keyless deployment operations.
///
/// Most variants map directly to Solidity errors defined in `IKeylessDeploy`.
/// Internal-only variants are mapped at the ABI boundary by `encode_error_result`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KeylessDeployError {
/// The transaction data is malformed (invalid RLP encoding)
MalformedEncoding,
/// The transaction is not a contract creation (to address is not empty)
NotContractCreation,
/// The transaction is not pre-EIP-155 (v must be 27 or 28)
NotPreEIP155,
/// The nonce in the signed transaction is not zero
NonZeroTxNonce {
/// The nonce value in the signed transaction
tx_nonce: u64,
},
/// The call tried to transfer ether (maps to `NoEtherTransfer`)
NoEtherTransfer,
/// Failed to recover signer from signature (invalid signature)
InvalidSignature,
/// The signer does not have enough balance to cover the sandbox tx's pre-execution
/// debit: `gas_limit × gas_price + value` on pre-Rex5 specs, `value` only on Rex5+
/// (where the sandbox tx is fee-free and only the `value` transfer needs funding).
InsufficientBalance,
/// The deploy address already has code (contract already exists)
ContractAlreadyExists,
/// The signer nonce is higher than allowed for keyless deploy
SignerNonceTooHigh {
/// The on-chain nonce of the recovered signer
signer_nonce: u64,
},
/// The sandbox execution reverted
ExecutionReverted {
/// The gas used
gas_used: u64,
/// The output
output: Bytes,
},
/// The sandbox execution halted (out of gas, stack overflow, etc.)
ExecutionHalted {
/// The gas used
gas_used: u64,
/// The reason
reason: MegaHaltReason,
},
/// Rex5 preflight rejected the call: the parent's remaining TX-level budget for some
/// dimension is smaller than the sandbox's known pre-frame intrinsic usage, so the
/// sandbox is guaranteed to fail internally and is not started.
///
/// Returned as a Revert (like other validation errors) since no sandbox execution
/// has started and no signer state needs to persist.
ParentBudgetExceeded {
/// The dimension whose parent remaining is too small.
kind: LimitKind,
/// The parent's remaining limit for that dimension, i.e. the cap the sandbox
/// would have been given.
limit: u64,
/// The sandbox's known pre-frame intrinsic usage for that dimension.
used: u64,
},
/// Contract creation succeeded but returned empty bytecode
EmptyCodeDeployed {
/// The gas used
gas_used: u64,
},
/// Contract creation succeeded but no address was returned (unexpected EVM behavior)
NoContractCreated,
/// The created contract address doesn't match the expected address (internal bug)
AddressMismatch,
/// The gas limit override is less than the gas limit in the keyless transaction
GasLimitTooLow {
/// The gas limit from the keyless transaction
tx_gas_limit: u64,
/// The gas limit override provided by the caller
provided_gas_limit: u64,
},
/// The remaining compute gas is insufficient to pay for the keyless deploy overhead.
InsufficientComputeGas {
/// The configured compute gas limit
limit: u64,
/// The actual compute gas usage
used: u64,
},
/// The keyless transaction's init code exceeds the configured maximum init code size.
///
/// Rex5+ only: the sandbox runs as an OP deposit-like transaction which bypasses
/// op-revm's `validate_env` (where revm's EIP-3860 size check lives), so the sandbox
/// must re-enforce the limit itself against `cfg().max_initcode_size()`.
InitCodeTooLarge {
/// The init code length in bytes.
size: u64,
/// The configured max init code size.
max: u64,
},
/// The recovered signer has non-empty, non-EIP-7702 bytecode in parent state.
///
/// Rex5+ only: the deposit-style sandbox bypasses op-revm's EIP-3607 check (which
/// normally lives in `validate_account_nonce_and_code`), so the sandbox enforces it
/// itself before constructing the sandbox transaction.
SignerHasCode,
/// Internal sandbox failure (DB I/O, header validation, etc.).
/// Selector-only — precompile return data is consensus-affecting, so the wire must
/// not pin consensus to upstream revm/op-revm `Display` impls.
InternalError,
/// Sandbox rejected the inner transaction as a tx-validation error
/// (`IsTxError::is_tx_error() == true`). A dedicated selector lets relayer-side
/// decoders distinguish this from a genuine internal failure. Selector-only for the
/// same consensus-decoupling reason as `InternalError`.
InvalidTransaction,
/// The keylessDeploy call was not intercepted (only returned by Solidity contract for inner
/// calls)
NotIntercepted,
}
/// Encodes a keyless deploy error as ABI-encoded revert data.
///
/// Uses the generated Solidity error bindings from IKeylessDeploy.sol.
pub fn encode_error_result(error: KeylessDeployError) -> Bytes {
match error {
KeylessDeployError::MalformedEncoding => {
IKeylessDeploy::MalformedEncoding {}.abi_encode().into()
}
KeylessDeployError::NotContractCreation => {
IKeylessDeploy::NotContractCreation {}.abi_encode().into()
}
KeylessDeployError::NotPreEIP155 => IKeylessDeploy::NotPreEIP155 {}.abi_encode().into(),
KeylessDeployError::NonZeroTxNonce { tx_nonce } => {
IKeylessDeploy::NonZeroTxNonce { txNonce: tx_nonce }.abi_encode().into()
}
KeylessDeployError::NoEtherTransfer => {
IKeylessDeploy::NoEtherTransfer {}.abi_encode().into()
}
KeylessDeployError::InvalidSignature => {
IKeylessDeploy::InvalidSignature {}.abi_encode().into()
}
KeylessDeployError::InsufficientBalance => {
IKeylessDeploy::InsufficientBalance {}.abi_encode().into()
}
KeylessDeployError::ContractAlreadyExists => {
IKeylessDeploy::ContractAlreadyExists {}.abi_encode().into()
}
KeylessDeployError::SignerNonceTooHigh { signer_nonce } => {
IKeylessDeploy::SignerNonceTooHigh { signerNonce: signer_nonce }.abi_encode().into()
}
KeylessDeployError::ExecutionReverted { gas_used, output } => {
IKeylessDeploy::ExecutionReverted { gasUsed: gas_used, output }.abi_encode().into()
}
KeylessDeployError::ExecutionHalted { gas_used, .. } => {
IKeylessDeploy::ExecutionHalted { gasUsed: gas_used }.abi_encode().into()
}
KeylessDeployError::ParentBudgetExceeded { kind, limit, used } => {
IKeylessDeploy::ParentBudgetExceeded { kind: kind.as_u8(), limit, used }
.abi_encode()
.into()
}
KeylessDeployError::EmptyCodeDeployed { gas_used } => {
IKeylessDeploy::EmptyCodeDeployed { gasUsed: gas_used }.abi_encode().into()
}
KeylessDeployError::NoContractCreated => {
IKeylessDeploy::NoContractCreated {}.abi_encode().into()
}
KeylessDeployError::AddressMismatch => {
IKeylessDeploy::AddressMismatch {}.abi_encode().into()
}
KeylessDeployError::GasLimitTooLow { tx_gas_limit, provided_gas_limit } => {
IKeylessDeploy::GasLimitTooLow {
txGasLimit: tx_gas_limit,
providedGasLimit: provided_gas_limit,
}
.abi_encode()
.into()
}
KeylessDeployError::InsufficientComputeGas { limit, used } => {
IKeylessDeploy::InsufficientComputeGas { limit, used }.abi_encode().into()
}
KeylessDeployError::InitCodeTooLarge { size, max } => {
IKeylessDeploy::InitCodeTooLarge { size, max }.abi_encode().into()
}
KeylessDeployError::SignerHasCode => IKeylessDeploy::SignerHasCode {}.abi_encode().into(),
KeylessDeployError::InternalError => IKeylessDeploy::InternalError {}.abi_encode().into(),
KeylessDeployError::InvalidTransaction => {
IKeylessDeploy::InvalidTransaction {}.abi_encode().into()
}
KeylessDeployError::NotIntercepted => IKeylessDeploy::NotIntercepted {}.abi_encode().into(),
}
}
/// Decodes ABI-encoded revert data into a `KeylessDeployError`.
///
/// Returns `None` if the data doesn't match any known error format.
///
/// Note: For `ExecutionHalted`, the halt reason cannot be recovered from ABI encoding,
/// so a default `OutOfGas` reason is used.
pub fn decode_error_result(output: &[u8]) -> Option<KeylessDeployError> {
if IKeylessDeploy::NoEtherTransfer::abi_decode(output).is_ok() {
return Some(KeylessDeployError::NoEtherTransfer);
}
if IKeylessDeploy::MalformedEncoding::abi_decode(output).is_ok() {
return Some(KeylessDeployError::MalformedEncoding);
}
if IKeylessDeploy::NotContractCreation::abi_decode(output).is_ok() {
return Some(KeylessDeployError::NotContractCreation);
}
if IKeylessDeploy::NotPreEIP155::abi_decode(output).is_ok() {
return Some(KeylessDeployError::NotPreEIP155);
}
if let Ok(e) = IKeylessDeploy::NonZeroTxNonce::abi_decode(output) {
return Some(KeylessDeployError::NonZeroTxNonce { tx_nonce: e.txNonce });
}
if IKeylessDeploy::InvalidSignature::abi_decode(output).is_ok() {
return Some(KeylessDeployError::InvalidSignature);
}
if IKeylessDeploy::InsufficientBalance::abi_decode(output).is_ok() {
return Some(KeylessDeployError::InsufficientBalance);
}
if IKeylessDeploy::ContractAlreadyExists::abi_decode(output).is_ok() {
return Some(KeylessDeployError::ContractAlreadyExists);
}
if let Ok(e) = IKeylessDeploy::SignerNonceTooHigh::abi_decode(output) {
return Some(KeylessDeployError::SignerNonceTooHigh { signer_nonce: e.signerNonce });
}
if let Ok(e) = IKeylessDeploy::ExecutionReverted::abi_decode(output) {
return Some(KeylessDeployError::ExecutionReverted {
gas_used: e.gasUsed,
output: e.output,
});
}
if let Ok(e) = IKeylessDeploy::ExecutionHalted::abi_decode(output) {
// Note: The actual halt reason is lost in ABI encoding, use OutOfGas as placeholder
return Some(KeylessDeployError::ExecutionHalted {
gas_used: e.gasUsed,
reason: MegaHaltReason::Base(op_revm::OpHaltReason::Base(
revm::context::result::HaltReason::OutOfGas(
revm::context::result::OutOfGasError::Basic,
),
)),
});
}
if let Ok(e) = IKeylessDeploy::ParentBudgetExceeded::abi_decode(output) {
return Some(KeylessDeployError::ParentBudgetExceeded {
kind: LimitKind::from_u8(e.kind)?,
limit: e.limit,
used: e.used,
});
}
if let Ok(e) = IKeylessDeploy::EmptyCodeDeployed::abi_decode(output) {
return Some(KeylessDeployError::EmptyCodeDeployed { gas_used: e.gasUsed });
}
if IKeylessDeploy::NoContractCreated::abi_decode(output).is_ok() {
return Some(KeylessDeployError::NoContractCreated);
}
if IKeylessDeploy::AddressMismatch::abi_decode(output).is_ok() {
return Some(KeylessDeployError::AddressMismatch);
}
if let Ok(e) = IKeylessDeploy::GasLimitTooLow::abi_decode(output) {
return Some(KeylessDeployError::GasLimitTooLow {
tx_gas_limit: e.txGasLimit,
provided_gas_limit: e.providedGasLimit,
});
}
if let Ok(e) = IKeylessDeploy::InitCodeTooLarge::abi_decode(output) {
return Some(KeylessDeployError::InitCodeTooLarge { size: e.size, max: e.max });
}
if IKeylessDeploy::SignerHasCode::abi_decode(output).is_ok() {
return Some(KeylessDeployError::SignerHasCode);
}
if IKeylessDeploy::InvalidTransaction::abi_decode(output).is_ok() {
return Some(KeylessDeployError::InvalidTransaction);
}
if IKeylessDeploy::InternalError::abi_decode(output).is_ok() {
return Some(KeylessDeployError::InternalError);
}
if IKeylessDeploy::NotIntercepted::abi_decode(output).is_ok() {
return Some(KeylessDeployError::NotIntercepted);
}
None
}
#[cfg(test)]
mod tests {
use super::*;
/// `InternalError` and `InvalidTransaction` carry no payload on the wire, so a
/// roundtrip MUST produce the same variant. Pinned because both selectors are
/// reachable on-chain via `RETURNDATACOPY` and any divergence between
/// `encode_error_result` and `decode_error_result` would break consensus.
#[test]
fn test_internal_error_roundtrip_is_selector_only() {
let encoded = encode_error_result(KeylessDeployError::InternalError);
// Selector-only: the encoded form is exactly the 4-byte Solidity selector.
assert_eq!(encoded.len(), 4, "InternalError must be selector-only");
assert!(matches!(decode_error_result(&encoded), Some(KeylessDeployError::InternalError)));
}
#[test]
fn test_invalid_transaction_roundtrip_is_selector_only() {
let encoded = encode_error_result(KeylessDeployError::InvalidTransaction);
assert_eq!(encoded.len(), 4, "InvalidTransaction must be selector-only");
assert!(matches!(
decode_error_result(&encoded),
Some(KeylessDeployError::InvalidTransaction)
));
}
/// `InitCodeTooLarge { size, max }` is a consensus-visible ABI selector. Pinning the
/// round-trip catches any drift between the Solidity error definition and the Rust
/// encode/decode (e.g. forgotten arm in `encode_error_result` or `decode_error_result`,
/// or accidental field reordering).
#[test]
fn test_init_code_too_large_roundtrip_preserves_size_and_max() {
let original = KeylessDeployError::InitCodeTooLarge { size: 600_000, max: 548_864 };
let encoded = encode_error_result(original.clone());
let decoded = decode_error_result(&encoded).expect("must decode");
assert_eq!(decoded, original);
}
/// `SignerHasCode` is selector-only; pinning the round-trip catches arm drift.
#[test]
fn test_signer_has_code_roundtrip_is_selector_only() {
let encoded = encode_error_result(KeylessDeployError::SignerHasCode);
assert_eq!(encoded.len(), 4, "SignerHasCode must be selector-only");
assert!(matches!(decode_error_result(&encoded), Some(KeylessDeployError::SignerHasCode)));
}
}