diff --git a/bin/morph-statetest/src/runner.rs b/bin/morph-statetest/src/runner.rs index a3c650b9..1fed0dc6 100644 --- a/bin/morph-statetest/src/runner.rs +++ b/bin/morph-statetest/src/runner.rs @@ -246,6 +246,15 @@ fn validation_error( where E: std::fmt::Display, { + // `expectException` is checked for presence, deliberately not for its text. + // go-ethereum's own statetest harness does the same -- `tests/state_test.go` + // returns early on `len(ExpectException) > 0` under a standing + // "TODO check error string" -- so matching on the text here would make this + // runner stricter than the client the fixtures are generated from, and a + // fixture imported from go-ethereum could fail on wording alone. The string + // stays in the JSON as documentation of which failure the case is meant to + // provoke; the assertion is that the transaction is *rejected*, which is what + // both clients agree on. match (&test.expect_exception, exec_result) { (Some(_), Err(_)) => return None, (Some(expected), Ok(_)) => { diff --git a/bin/morph-statetest/tests/fee_token_internal_calls.rs b/bin/morph-statetest/tests/fee_token_internal_calls.rs new file mode 100644 index 00000000..16958bad --- /dev/null +++ b/bin/morph-statetest/tests/fee_token_internal_calls.rs @@ -0,0 +1,76 @@ +//! Golden roots/logs generated by morph-geth (Emerald and Jade): the original twelve +//! cases on 5744b8f66, the three later ones on 4012f174b, which differs only in the +//! transaction-size limit in `core/tx_pool.go` and reproduces all twelve unchanged. +//! The gas constants are transaction totals; geth's statetest tool subtracts +//! intrinsic gas when the total is at least the intrinsic cost. +use morph_statetest::runner::run_suite_str; + +#[test] +fn fee_token_calls_match_geth() { + let outcomes = run_suite_str(include_str!("fixtures/fee_token_internal_calls.json")).unwrap(); + assert_eq!(outcomes.len(), 30); + for outcome in outcomes { + assert!( + outcome.pass, + "{} / {}: {}", + outcome.test, outcome.fork, outcome.error_msg + ); + let gas = match outcome.test.as_str() { + "deduct_clear" => 16_800, + "main_revert" => 16_804, + "main_oog" => 95_200, + "main_restores_cleared_slot" => 23_291, + "balance_writes" => 0, + "zero_fee_reads_balance" => 23_574, + // The transaction's own frame reads MSIZE and MLOAD(0) into storage before + // writing either. go-ethereum allocates a fresh `Memory` per interpreter run, + // so it sees zeros and neither SSTORE changes state; a client whose fee frames + // leave their bytes on the transaction's memory writes two non-zero slots and + // misses this root by two SSTORE_SETs. + "main_reads_uninitialized_memory" => 25_417, + // The registry's direct-slot path. `slot_deduct_clear` is the same transaction + // as `deduct_clear`, which costs 16_800: clearing the payer's balance through a + // real `transfer` books a `+4800` SSTORE refund that reaches the transaction, + // while `SetState` books nothing. That 4_200 is the only way the two modes bill + // differently. + "slot_deduct_keep" | "slot_deduct_clear" => 21_000, + "deduct_keep" + | "origin_guard" + | "gasprice_guard" + | "refund_false_keeps_transfer" + | "zero_fee" + | "refund_revert_rolls_back_transfer" => 21_000, + name => panic!("missing gas expectation for {name}"), + }; + assert_eq!(outcome.gas_used, gas, "{} / {}", outcome.test, outcome.fork); + } +} + +/// Replays mainnet transaction `0x9ebfdac9040d7c2a8739ffdaae8baf5e7aa22fdb48585f80592de4b4cf39ed44` +/// (block 26836567, Jade): a MorphTx V0 that pays its fee in token 1, which the registry still +/// resolves through the direct-slot path, and whose call transfers that same token. The fixtures +/// above are synthetic; this is slot mode as mainnet actually ran it, which every node must keep +/// replaying identically after the registry moves off slot mode. +/// +/// go-ethereum's state-test runner signs with `secretKey` and fixes the chain id to 1, so the +/// sender is the harness account `0xa94f…6ebf0b`, carrying the real sender's nonce, EIP-7702 +/// delegation code and token balance under its own balance slot, and the fee vault's balance sits +/// under the harness vault. The prestate tracer does not see the slots the fee logic reads straight +/// from state, so the token balances, the registry entry and the L1 gas price oracle come from the +/// parent block, which is exact here because the transaction is alone in its block. Roots are from +/// morph-geth 4012f174b. The logs root equals the on-chain receipt's logs with the sender topic +/// substituted, and the gas matches the on-chain receipt: the substitution changes the L1 data fee, +/// and with it the token amount charged, but not the gas burned. +#[test] +fn mainnet_slot_mode_fee_token_transfer_matches_geth() { + let outcomes = + run_suite_str(include_str!("fixtures/mainnet_slot_mode_fee_token.json")).unwrap(); + assert_eq!(outcomes.len(), 1); + let outcome = &outcomes[0]; + assert!( + outcome.pass, + "{} / {}: {}", + outcome.test, outcome.fork, outcome.error_msg + ); + assert_eq!(outcome.gas_used, 51_257); +} diff --git a/bin/morph-statetest/tests/fixtures/fee_token_internal_calls.json b/bin/morph-statetest/tests/fixtures/fee_token_internal_calls.json new file mode 100644 index 00000000..c8ca1348 --- /dev/null +++ b/bin/morph-statetest/tests/fixtures/fee_token_internal_calls.json @@ -0,0 +1,1324 @@ +{ + "balance_writes": { + "env": { + "currentCoinbase": "0x530000000000000000000000000000000000000a", + "currentDifficulty": "0x0", + "currentGasLimit": "0x1c9c380", + "currentNumber": "0x1", + "currentTimestamp": "0x1", + "currentBaseFee": "0x1", + "currentChainID": "0x1" + }, + "pre": { + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0xde0b6b3a7640000", + "nonce": "0x0", + "code": "0x", + "storage": {} + }, + "0x3000000000000000000000000000000000000003": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x60015f55620f42405f5260205ff3", + "storage": { + "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b": "0x00000000000000000000000000000000000000000000000000000000000f4240" + } + }, + "0x4200000000000000000000000000000000000042": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": {} + }, + "0x5300000000000000000000000000000000000021": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": { + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706d": "0x0000000000000000000000003000000000000000000000000000000000000003", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706e": "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706f": "0x0000000000000000000000000000000000000000000000000000000000001201", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e6377070": "0x0000000000000000000000000000000000000000000000000000000000000001", + "0xbb86fbc034f4e382929974bcd8419ed626b0ea647f962d89ba2fb6bd28785ab9": "0x0000000000000000000000000000000000000000000000000000000000000001" + } + } + }, + "transaction": { + "type": "0x7f", + "version": "0x0", + "feeTokenID": "0x1", + "feeLimit": "0x0", + "nonce": "0x0", + "gasPrice": "0xa", + "gasLimit": [ + "0x186a0" + ], + "to": "0x4200000000000000000000000000000000000042", + "value": [ + "0x0" + ], + "data": [ + "0x" + ], + "secretKey": "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" + }, + "post": { + "Emerald": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0x7c1c360fabaec3c980927e916a02d97a6ef79b93d601e0a1616e4a8014ce5669", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "expectException": "TokenBalanceQueryFailed" + } + ], + "Jade": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0x7c1c360fabaec3c980927e916a02d97a6ef79b93d601e0a1616e4a8014ce5669", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "expectException": "TokenBalanceQueryFailed" + } + ] + } + }, + "deduct_clear": { + "env": { + "currentCoinbase": "0x530000000000000000000000000000000000000a", + "currentDifficulty": "0x0", + "currentGasLimit": "0x1c9c380", + "currentNumber": "0x1", + "currentTimestamp": "0x1", + "currentBaseFee": "0x1", + "currentChainID": "0x1" + }, + "pre": { + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0xde0b6b3a7640000", + "nonce": "0x0", + "code": "0x", + "storage": {} + }, + "0x3000000000000000000000000000000000000003": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x366044146013576004355460005260206000f35b60243580335403335560043580548201905550600160005260206000f3", + "storage": { + "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b": "0x00000000000000000000000000000000000000000000000000000000000f4240" + } + }, + "0x4200000000000000000000000000000000000042": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": {} + }, + "0x5300000000000000000000000000000000000021": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": { + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706d": "0x0000000000000000000000003000000000000000000000000000000000000003", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706e": "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706f": "0x0000000000000000000000000000000000000000000000000000000000001201", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e6377070": "0x0000000000000000000000000000000000000000000000000000000000000001", + "0xbb86fbc034f4e382929974bcd8419ed626b0ea647f962d89ba2fb6bd28785ab9": "0x0000000000000000000000000000000000000000000000000000000000000001" + } + } + }, + "transaction": { + "type": "0x7f", + "version": "0x0", + "feeTokenID": "0x1", + "feeLimit": "0x0", + "nonce": "0x0", + "gasPrice": "0xa", + "gasLimit": [ + "0x186a0" + ], + "to": "0x4200000000000000000000000000000000000042", + "value": [ + "0x0" + ], + "data": [ + "0x" + ], + "secretKey": "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" + }, + "post": { + "Emerald": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0xfb0ac627f1b92b10d0f1c60ef87667dc42cd3eb7a919ceb84e9427c63f9b6125", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "Jade": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0xfb0ac627f1b92b10d0f1c60ef87667dc42cd3eb7a919ceb84e9427c63f9b6125", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ] + } + }, + "deduct_keep": { + "env": { + "currentCoinbase": "0x530000000000000000000000000000000000000a", + "currentDifficulty": "0x0", + "currentGasLimit": "0x1c9c380", + "currentNumber": "0x1", + "currentTimestamp": "0x1", + "currentBaseFee": "0x1", + "currentChainID": "0x1" + }, + "pre": { + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0xde0b6b3a7640000", + "nonce": "0x0", + "code": "0x", + "storage": {} + }, + "0x3000000000000000000000000000000000000003": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x366044146013576004355460005260206000f35b60243580335403335560043580548201905550600160005260206000f3", + "storage": { + "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b": "0x00000000000000000000000000000000000000000000000000000000000f4241" + } + }, + "0x4200000000000000000000000000000000000042": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": {} + }, + "0x5300000000000000000000000000000000000021": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": { + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706d": "0x0000000000000000000000003000000000000000000000000000000000000003", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706e": "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706f": "0x0000000000000000000000000000000000000000000000000000000000001201", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e6377070": "0x0000000000000000000000000000000000000000000000000000000000000001", + "0xbb86fbc034f4e382929974bcd8419ed626b0ea647f962d89ba2fb6bd28785ab9": "0x0000000000000000000000000000000000000000000000000000000000000001" + } + } + }, + "transaction": { + "type": "0x7f", + "version": "0x0", + "feeTokenID": "0x1", + "feeLimit": "0x0", + "nonce": "0x0", + "gasPrice": "0xa", + "gasLimit": [ + "0x186a0" + ], + "to": "0x4200000000000000000000000000000000000042", + "value": [ + "0x0" + ], + "data": [ + "0x" + ], + "secretKey": "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" + }, + "post": { + "Emerald": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0xe1a1a7e57eaf4aee439d65814e0a688a2775ee81fbdc2c4d77f06534d7088eac", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "Jade": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0xe1a1a7e57eaf4aee439d65814e0a688a2775ee81fbdc2c4d77f06534d7088eac", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ] + } + }, + "gasprice_guard": { + "env": { + "currentCoinbase": "0x530000000000000000000000000000000000000a", + "currentDifficulty": "0x0", + "currentGasLimit": "0x1c9c380", + "currentNumber": "0x1", + "currentTimestamp": "0x1", + "currentBaseFee": "0x1", + "currentChainID": "0x1" + }, + "pre": { + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0xde0b6b3a7640000", + "nonce": "0x0", + "code": "0x", + "storage": {} + }, + "0x3000000000000000000000000000000000000003": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x3a600a14600a575f5ffd5b36604414601e576004355460005260206000f35b60243580335403335560043580548201905550600160005260206000f3", + "storage": { + "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b": "0x00000000000000000000000000000000000000000000000000000000000f4241" + } + }, + "0x4200000000000000000000000000000000000042": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": {} + }, + "0x5300000000000000000000000000000000000021": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": { + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706d": "0x0000000000000000000000003000000000000000000000000000000000000003", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706e": "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706f": "0x0000000000000000000000000000000000000000000000000000000000001201", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e6377070": "0x0000000000000000000000000000000000000000000000000000000000000001", + "0xbb86fbc034f4e382929974bcd8419ed626b0ea647f962d89ba2fb6bd28785ab9": "0x0000000000000000000000000000000000000000000000000000000000000001" + } + } + }, + "transaction": { + "type": "0x7f", + "version": "0x0", + "feeTokenID": "0x1", + "feeLimit": "0x0", + "nonce": "0x0", + "gasPrice": "0xa", + "gasLimit": [ + "0x186a0" + ], + "to": "0x4200000000000000000000000000000000000042", + "value": [ + "0x0" + ], + "data": [ + "0x" + ], + "secretKey": "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" + }, + "post": { + "Emerald": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0x0d1c7d12041d949d73f15b8d08dbdcacecdf844675d40d41632a0bfb463ef36d", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "Jade": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0x0d1c7d12041d949d73f15b8d08dbdcacecdf844675d40d41632a0bfb463ef36d", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ] + } + }, + "main_oog": { + "env": { + "currentCoinbase": "0x530000000000000000000000000000000000000a", + "currentDifficulty": "0x0", + "currentGasLimit": "0x1c9c380", + "currentNumber": "0x1", + "currentTimestamp": "0x1", + "currentBaseFee": "0x1", + "currentChainID": "0x1" + }, + "pre": { + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0xde0b6b3a7640000", + "nonce": "0x0", + "code": "0x", + "storage": {} + }, + "0x3000000000000000000000000000000000000003": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x366044146013576004355460005260206000f35b60243580335403335560043580548201905550600160005260206000f3", + "storage": { + "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b": "0x00000000000000000000000000000000000000000000000000000000000f4240" + } + }, + "0x4200000000000000000000000000000000000042": { + "balance": "0x0", + "nonce": "0x0", + "code": "0xfe", + "storage": {} + }, + "0x5300000000000000000000000000000000000021": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": { + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706d": "0x0000000000000000000000003000000000000000000000000000000000000003", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706e": "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706f": "0x0000000000000000000000000000000000000000000000000000000000001201", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e6377070": "0x0000000000000000000000000000000000000000000000000000000000000001", + "0xbb86fbc034f4e382929974bcd8419ed626b0ea647f962d89ba2fb6bd28785ab9": "0x0000000000000000000000000000000000000000000000000000000000000001" + } + } + }, + "transaction": { + "type": "0x7f", + "version": "0x0", + "feeTokenID": "0x1", + "feeLimit": "0x0", + "nonce": "0x0", + "gasPrice": "0xa", + "gasLimit": [ + "0x186a0" + ], + "to": "0x4200000000000000000000000000000000000042", + "value": [ + "0x0" + ], + "data": [ + "0x" + ], + "secretKey": "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" + }, + "post": { + "Emerald": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0x1b9c3313759258951e68ab4e7754f0859f0b049431eb3d1e7a2ff460ceafe82b", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "Jade": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0x1b9c3313759258951e68ab4e7754f0859f0b049431eb3d1e7a2ff460ceafe82b", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ] + } + }, + "main_reads_uninitialized_memory": { + "env": { + "currentCoinbase": "0x530000000000000000000000000000000000000a", + "currentDifficulty": "0x0", + "currentGasLimit": "0x1c9c380", + "currentNumber": "0x1", + "currentTimestamp": "0x1", + "currentBaseFee": "0x1", + "currentChainID": "0x1" + }, + "pre": { + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0xde0b6b3a7640000", + "nonce": "0x0", + "code": "0x", + "storage": {} + }, + "0x3000000000000000000000000000000000000003": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x366044146013576004355460005260206000f35b60243580335403335560043580548201905550600160005260206000f3", + "storage": { + "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b": "0x00000000000000000000000000000000000000000000000000000000000f4241" + } + }, + "0x4200000000000000000000000000000000000042": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x5960005560005160015500", + "storage": {} + }, + "0x5300000000000000000000000000000000000021": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": { + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706d": "0x0000000000000000000000003000000000000000000000000000000000000003", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706e": "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706f": "0x0000000000000000000000000000000000000000000000000000000000001201", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e6377070": "0x0000000000000000000000000000000000000000000000000000000000000001", + "0xbb86fbc034f4e382929974bcd8419ed626b0ea647f962d89ba2fb6bd28785ab9": "0x0000000000000000000000000000000000000000000000000000000000000001" + } + } + }, + "transaction": { + "type": "0x7f", + "version": "0x0", + "feeTokenID": "0x1", + "feeLimit": "0x0", + "nonce": "0x0", + "gasPrice": "0xa", + "gasLimit": [ + "0x186a0" + ], + "to": "0x4200000000000000000000000000000000000042", + "value": [ + "0x0" + ], + "data": [ + "0x" + ], + "secretKey": "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" + }, + "post": { + "Emerald": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0xb05c0ccb41d7ad9b13f5e8ac1b93f42dc8c8b8bbd312e0af5f91fa5525f47a49", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "Jade": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0xb05c0ccb41d7ad9b13f5e8ac1b93f42dc8c8b8bbd312e0af5f91fa5525f47a49", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ] + } + }, + "main_restores_cleared_slot": { + "env": { + "currentCoinbase": "0x530000000000000000000000000000000000000a", + "currentDifficulty": "0x0", + "currentGasLimit": "0x1c9c380", + "currentNumber": "0x1", + "currentTimestamp": "0x1", + "currentBaseFee": "0x1", + "currentChainID": "0x1" + }, + "pre": { + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0xde0b6b3a7640000", + "nonce": "0x0", + "code": "0x", + "storage": {} + }, + "0x3000000000000000000000000000000000000003": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x3660041460385736604414601a576004355460005260206000f35b60243580335403335560043580548201905550600160005260206000f35b6001325500", + "storage": { + "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b": "0x00000000000000000000000000000000000000000000000000000000000f4240" + } + }, + "0x4200000000000000000000000000000000000042": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": {} + }, + "0x5300000000000000000000000000000000000021": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": { + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706d": "0x0000000000000000000000003000000000000000000000000000000000000003", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706e": "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706f": "0x0000000000000000000000000000000000000000000000000000000000001201", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e6377070": "0x0000000000000000000000000000000000000000000000000000000000000001", + "0xbb86fbc034f4e382929974bcd8419ed626b0ea647f962d89ba2fb6bd28785ab9": "0x0000000000000000000000000000000000000000000000000000000000000001" + } + } + }, + "transaction": { + "type": "0x7f", + "version": "0x0", + "feeTokenID": "0x1", + "feeLimit": "0x0", + "nonce": "0x0", + "gasPrice": "0xa", + "gasLimit": [ + "0x186a0" + ], + "to": "0x3000000000000000000000000000000000000003", + "value": [ + "0x0" + ], + "data": [ + "0xdeadbeef" + ], + "secretKey": "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" + }, + "post": { + "Emerald": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0xdda0e3d5c58b32c62b9168cf468233be793eb5d1d0ab886dacdaf490b84a72e2", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "Jade": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0xdda0e3d5c58b32c62b9168cf468233be793eb5d1d0ab886dacdaf490b84a72e2", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ] + } + }, + "main_revert": { + "env": { + "currentCoinbase": "0x530000000000000000000000000000000000000a", + "currentDifficulty": "0x0", + "currentGasLimit": "0x1c9c380", + "currentNumber": "0x1", + "currentTimestamp": "0x1", + "currentBaseFee": "0x1", + "currentChainID": "0x1" + }, + "pre": { + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0xde0b6b3a7640000", + "nonce": "0x0", + "code": "0x", + "storage": {} + }, + "0x3000000000000000000000000000000000000003": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x366044146013576004355460005260206000f35b60243580335403335560043580548201905550600160005260206000f3", + "storage": { + "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b": "0x00000000000000000000000000000000000000000000000000000000000f4240" + } + }, + "0x4200000000000000000000000000000000000042": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x5f5ffd", + "storage": {} + }, + "0x5300000000000000000000000000000000000021": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": { + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706d": "0x0000000000000000000000003000000000000000000000000000000000000003", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706e": "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706f": "0x0000000000000000000000000000000000000000000000000000000000001201", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e6377070": "0x0000000000000000000000000000000000000000000000000000000000000001", + "0xbb86fbc034f4e382929974bcd8419ed626b0ea647f962d89ba2fb6bd28785ab9": "0x0000000000000000000000000000000000000000000000000000000000000001" + } + } + }, + "transaction": { + "type": "0x7f", + "version": "0x0", + "feeTokenID": "0x1", + "feeLimit": "0x0", + "nonce": "0x0", + "gasPrice": "0xa", + "gasLimit": [ + "0x186a0" + ], + "to": "0x4200000000000000000000000000000000000042", + "value": [ + "0x0" + ], + "data": [ + "0x" + ], + "secretKey": "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" + }, + "post": { + "Emerald": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0x4bfefc48ad519124f544514a1d413329e38fce98ee522a763a79fdd96b68fdc2", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "Jade": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0x4bfefc48ad519124f544514a1d413329e38fce98ee522a763a79fdd96b68fdc2", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ] + } + }, + "origin_guard": { + "env": { + "currentCoinbase": "0x530000000000000000000000000000000000000a", + "currentDifficulty": "0x0", + "currentGasLimit": "0x1c9c380", + "currentNumber": "0x1", + "currentTimestamp": "0x1", + "currentBaseFee": "0x1", + "currentChainID": "0x1" + }, + "pre": { + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0xde0b6b3a7640000", + "nonce": "0x0", + "code": "0x", + "storage": {} + }, + "0x3000000000000000000000000000000000000003": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x3273a94f5374fce5edbc8e2a8697c15331677e6ebf0b14601d575f5ffd5b366044146031576004355460005260206000f35b60243580335403335560043580548201905550600160005260206000f3", + "storage": { + "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b": "0x00000000000000000000000000000000000000000000000000000000000f4241" + } + }, + "0x4200000000000000000000000000000000000042": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": {} + }, + "0x5300000000000000000000000000000000000021": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": { + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706d": "0x0000000000000000000000003000000000000000000000000000000000000003", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706e": "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706f": "0x0000000000000000000000000000000000000000000000000000000000001201", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e6377070": "0x0000000000000000000000000000000000000000000000000000000000000001", + "0xbb86fbc034f4e382929974bcd8419ed626b0ea647f962d89ba2fb6bd28785ab9": "0x0000000000000000000000000000000000000000000000000000000000000001" + } + } + }, + "transaction": { + "type": "0x7f", + "version": "0x0", + "feeTokenID": "0x1", + "feeLimit": "0x0", + "nonce": "0x0", + "gasPrice": "0xa", + "gasLimit": [ + "0x186a0" + ], + "to": "0x4200000000000000000000000000000000000042", + "value": [ + "0x0" + ], + "data": [ + "0x" + ], + "secretKey": "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" + }, + "post": { + "Emerald": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0x3ed6676fa11fc0e4294ed8a446546a0c3c43f5b664bfa28f4104489d6390102a", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "Jade": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0x3ed6676fa11fc0e4294ed8a446546a0c3c43f5b664bfa28f4104489d6390102a", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ] + } + }, + "refund_false_keeps_transfer": { + "env": { + "currentCoinbase": "0x530000000000000000000000000000000000000a", + "currentDifficulty": "0x0", + "currentGasLimit": "0x1c9c380", + "currentNumber": "0x1", + "currentTimestamp": "0x1", + "currentBaseFee": "0x1", + "currentChainID": "0x1" + }, + "pre": { + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0xde0b6b3a7640000", + "nonce": "0x0", + "code": "0x", + "storage": {} + }, + "0x3000000000000000000000000000000000000003": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x366044146013576004355460005260206000f35b602435803354033355600435805482019055505f5fa03373a94f5374fce5edbc8e2a8697c15331677e6ebf0b145f5260205ff3", + "storage": { + "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b": "0x00000000000000000000000000000000000000000000000000000000000f4241" + } + }, + "0x4200000000000000000000000000000000000042": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": {} + }, + "0x5300000000000000000000000000000000000021": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": { + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706d": "0x0000000000000000000000003000000000000000000000000000000000000003", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706e": "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706f": "0x0000000000000000000000000000000000000000000000000000000000001201", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e6377070": "0x0000000000000000000000000000000000000000000000000000000000000001", + "0xbb86fbc034f4e382929974bcd8419ed626b0ea647f962d89ba2fb6bd28785ab9": "0x0000000000000000000000000000000000000000000000000000000000000001" + } + } + }, + "transaction": { + "type": "0x7f", + "version": "0x0", + "feeTokenID": "0x1", + "feeLimit": "0x0", + "nonce": "0x0", + "gasPrice": "0xa", + "gasLimit": [ + "0x186a0" + ], + "to": "0x4200000000000000000000000000000000000042", + "value": [ + "0x0" + ], + "data": [ + "0x" + ], + "secretKey": "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" + }, + "post": { + "Emerald": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0x02e23c9ed829c4eb474097e4babff09ad0214f7d1f4affe7f661f4927ecba46c", + "logs": "0x773911b8633886efdf9360b2936f4f0ee4ae34b8cd1c0ffd0c06861d1c751aeb" + } + ], + "Jade": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0x02e23c9ed829c4eb474097e4babff09ad0214f7d1f4affe7f661f4927ecba46c", + "logs": "0x773911b8633886efdf9360b2936f4f0ee4ae34b8cd1c0ffd0c06861d1c751aeb" + } + ] + } + }, + "refund_revert_rolls_back_transfer": { + "env": { + "currentCoinbase": "0x530000000000000000000000000000000000000a", + "currentDifficulty": "0x0", + "currentGasLimit": "0x1c9c380", + "currentNumber": "0x1", + "currentTimestamp": "0x1", + "currentBaseFee": "0x1", + "currentChainID": "0x1" + }, + "pre": { + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0xde0b6b3a7640000", + "nonce": "0x0", + "code": "0x", + "storage": {} + }, + "0x3000000000000000000000000000000000000003": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x366044146013576004355460005260206000f35b602435803354033355600435805482019055505f5fa03373a94f5374fce5edbc8e2a8697c15331677e6ebf0b146047575f5ffd5b600160005260206000f3", + "storage": { + "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b": "0x00000000000000000000000000000000000000000000000000000000000f4241" + } + }, + "0x4200000000000000000000000000000000000042": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": {} + }, + "0x5300000000000000000000000000000000000021": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": { + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706d": "0x0000000000000000000000003000000000000000000000000000000000000003", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706e": "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706f": "0x0000000000000000000000000000000000000000000000000000000000001201", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e6377070": "0x0000000000000000000000000000000000000000000000000000000000000001", + "0xbb86fbc034f4e382929974bcd8419ed626b0ea647f962d89ba2fb6bd28785ab9": "0x0000000000000000000000000000000000000000000000000000000000000001" + } + } + }, + "transaction": { + "type": "0x7f", + "version": "0x0", + "feeTokenID": "0x1", + "feeLimit": "0x0", + "nonce": "0x0", + "gasPrice": "0xa", + "gasLimit": [ + "0x186a0" + ], + "to": "0x4200000000000000000000000000000000000042", + "value": [ + "0x0" + ], + "data": [ + "0x" + ], + "secretKey": "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" + }, + "post": { + "Emerald": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0x0b438cf7a8bddc48e5097828159eed83ccdf052406ad296ec69cb8e194de0737", + "logs": "0x16ecc5d734ad798966d0ece89ce9fc365a53ed550661fb7607afee69c831daab" + } + ], + "Jade": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0x0b438cf7a8bddc48e5097828159eed83ccdf052406ad296ec69cb8e194de0737", + "logs": "0x16ecc5d734ad798966d0ece89ce9fc365a53ed550661fb7607afee69c831daab" + } + ] + } + }, + "slot_deduct_clear": { + "env": { + "currentCoinbase": "0x530000000000000000000000000000000000000a", + "currentDifficulty": "0x0", + "currentGasLimit": "0x1c9c380", + "currentNumber": "0x1", + "currentTimestamp": "0x1", + "currentBaseFee": "0x1", + "currentChainID": "0x1" + }, + "pre": { + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0xde0b6b3a7640000", + "nonce": "0x0", + "code": "0x", + "storage": {} + }, + "0x3000000000000000000000000000000000000003": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x366044146013576004355460005260206000f35b60243580335403335560043580548201905550600160005260206000f3", + "storage": { + "0x9734b052146069605dcf2a05300c1dd5cd5852a2844e5491b2eb25d6daa909bc": "0x00000000000000000000000000000000000000000000000000000000000f4240" + } + }, + "0x4200000000000000000000000000000000000042": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": {} + }, + "0x5300000000000000000000000000000000000021": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": { + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706d": "0x0000000000000000000000003000000000000000000000000000000000000003", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706e": "0x0000000000000000000000000000000000000000000000000000000000000002", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706f": "0x0000000000000000000000000000000000000000000000000000000000001201", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e6377070": "0x0000000000000000000000000000000000000000000000000000000000000001", + "0xbb86fbc034f4e382929974bcd8419ed626b0ea647f962d89ba2fb6bd28785ab9": "0x0000000000000000000000000000000000000000000000000000000000000001" + } + } + }, + "transaction": { + "type": "0x7f", + "version": "0x0", + "feeTokenID": "0x1", + "feeLimit": "0x0", + "nonce": "0x0", + "gasPrice": "0xa", + "gasLimit": [ + "0x186a0" + ], + "to": "0x4200000000000000000000000000000000000042", + "value": [ + "0x0" + ], + "data": [ + "0x" + ], + "secretKey": "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" + }, + "post": { + "Emerald": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0x84e1019fa81970b8488a94f2651174f4b8d901e6d71c51a3f2d43bc1b2d2736f", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "Jade": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0x84e1019fa81970b8488a94f2651174f4b8d901e6d71c51a3f2d43bc1b2d2736f", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ] + } + }, + "slot_deduct_keep": { + "env": { + "currentCoinbase": "0x530000000000000000000000000000000000000a", + "currentDifficulty": "0x0", + "currentGasLimit": "0x1c9c380", + "currentNumber": "0x1", + "currentTimestamp": "0x1", + "currentBaseFee": "0x1", + "currentChainID": "0x1" + }, + "pre": { + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0xde0b6b3a7640000", + "nonce": "0x0", + "code": "0x", + "storage": {} + }, + "0x3000000000000000000000000000000000000003": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x366044146013576004355460005260206000f35b60243580335403335560043580548201905550600160005260206000f3", + "storage": { + "0x9734b052146069605dcf2a05300c1dd5cd5852a2844e5491b2eb25d6daa909bc": "0x00000000000000000000000000000000000000000000000000000000000f4241" + } + }, + "0x4200000000000000000000000000000000000042": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": {} + }, + "0x5300000000000000000000000000000000000021": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": { + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706d": "0x0000000000000000000000003000000000000000000000000000000000000003", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706e": "0x0000000000000000000000000000000000000000000000000000000000000002", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706f": "0x0000000000000000000000000000000000000000000000000000000000001201", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e6377070": "0x0000000000000000000000000000000000000000000000000000000000000001", + "0xbb86fbc034f4e382929974bcd8419ed626b0ea647f962d89ba2fb6bd28785ab9": "0x0000000000000000000000000000000000000000000000000000000000000001" + } + } + }, + "transaction": { + "type": "0x7f", + "version": "0x0", + "feeTokenID": "0x1", + "feeLimit": "0x0", + "nonce": "0x0", + "gasPrice": "0xa", + "gasLimit": [ + "0x186a0" + ], + "to": "0x4200000000000000000000000000000000000042", + "value": [ + "0x0" + ], + "data": [ + "0x" + ], + "secretKey": "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" + }, + "post": { + "Emerald": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0xd4d266ff3372ea95e9fd510e2e3be04679fdb72b83671acf29222402bdb4e0e8", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "Jade": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0xd4d266ff3372ea95e9fd510e2e3be04679fdb72b83671acf29222402bdb4e0e8", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ] + } + }, + "zero_fee": { + "env": { + "currentCoinbase": "0x530000000000000000000000000000000000000a", + "currentDifficulty": "0x0", + "currentGasLimit": "0x1c9c380", + "currentNumber": "0x1", + "currentTimestamp": "0x1", + "currentBaseFee": "0x0", + "currentChainID": "0x1" + }, + "pre": { + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0xde0b6b3a7640000", + "nonce": "0x0", + "code": "0x", + "storage": {} + }, + "0x3000000000000000000000000000000000000003": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x366044146013576004355460005260206000f35b602435803354033355600435805482019055505f5fa0600160005260206000f3", + "storage": { + "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + }, + "0x4200000000000000000000000000000000000042": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": {} + }, + "0x5300000000000000000000000000000000000021": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": { + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706d": "0x0000000000000000000000003000000000000000000000000000000000000003", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706e": "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706f": "0x0000000000000000000000000000000000000000000000000000000000001201", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e6377070": "0x0000000000000000000000000000000000000000000000000000000000000001", + "0xbb86fbc034f4e382929974bcd8419ed626b0ea647f962d89ba2fb6bd28785ab9": "0x0000000000000000000000000000000000000000000000000000000000000001" + } + } + }, + "transaction": { + "type": "0x7f", + "version": "0x0", + "feeTokenID": "0x1", + "feeLimit": "0x0", + "nonce": "0x0", + "gasPrice": "0x0", + "gasLimit": [ + "0x186a0" + ], + "to": "0x4200000000000000000000000000000000000042", + "value": [ + "0x0" + ], + "data": [ + "0x" + ], + "secretKey": "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" + }, + "post": { + "Emerald": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0x316b400ab83009d30ab6a987f74a061de29c2f3116918ca244b7267a69b75be2", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "Jade": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0x316b400ab83009d30ab6a987f74a061de29c2f3116918ca244b7267a69b75be2", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ] + } + }, + "zero_fee_reads_balance": { + "env": { + "currentCoinbase": "0x530000000000000000000000000000000000000a", + "currentDifficulty": "0x0", + "currentGasLimit": "0x1c9c380", + "currentNumber": "0x1", + "currentTimestamp": "0x1", + "currentBaseFee": "0x0", + "currentChainID": "0x1" + }, + "pre": { + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0xde0b6b3a7640000", + "nonce": "0x0", + "code": "0x", + "storage": {} + }, + "0x3000000000000000000000000000000000000003": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x366044146013576004355460005260206000f35b60243580335403335560043580548201905550600160005260206000f3", + "storage": { + "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + }, + "0x4200000000000000000000000000000000000042": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": {} + }, + "0x5300000000000000000000000000000000000021": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": { + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706d": "0x0000000000000000000000003000000000000000000000000000000000000003", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706e": "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706f": "0x0000000000000000000000000000000000000000000000000000000000001201", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e6377070": "0x0000000000000000000000000000000000000000000000000000000000000001", + "0xbb86fbc034f4e382929974bcd8419ed626b0ea647f962d89ba2fb6bd28785ab9": "0x0000000000000000000000000000000000000000000000000000000000000001" + } + } + }, + "transaction": { + "type": "0x7f", + "version": "0x0", + "feeTokenID": "0x1", + "feeLimit": "0x0", + "nonce": "0x0", + "gasPrice": "0x0", + "gasLimit": [ + "0x186a0" + ], + "to": "0x3000000000000000000000000000000000000003", + "value": [ + "0x0" + ], + "data": [ + "0x70a08231000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b" + ], + "secretKey": "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" + }, + "post": { + "Emerald": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0xeb101e868f55079346e3fc2d78473e1ebf0b446070185d3a266f8f0f81f534a0", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "Jade": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0xeb101e868f55079346e3fc2d78473e1ebf0b446070185d3a266f8f0f81f534a0", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ] + } + } +} diff --git a/bin/morph-statetest/tests/fixtures/mainnet_slot_mode_fee_token.json b/bin/morph-statetest/tests/fixtures/mainnet_slot_mode_fee_token.json new file mode 100644 index 00000000..30868ac0 --- /dev/null +++ b/bin/morph-statetest/tests/fixtures/mainnet_slot_mode_fee_token.json @@ -0,0 +1,96 @@ +{ + "mainnet_26836567_slot_mode_fee_token_transfer": { + "env": { + "currentCoinbase": "0x530000000000000000000000000000000000000a", + "currentDifficulty": "0x0", + "currentGasLimit": "0x2aea540", + "currentNumber": "0x1997e57", + "currentTimestamp": "0x6aa9b017", + "currentBaseFee": "0xf4240", + "currentChainID": "0x1" + }, + "pre": { + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0x0", + "nonce": "0x1", + "code": "0xef0100a845c74344fc9405b1fcf712f04668979573c1bf", + "storage": {} + }, + "0xc7d67a9cbb121b3b0b9c053dd9f469523243379a": { + "balance": "0x0", + "nonce": "0x1", + "code": "0x363d3d373d3d3d363d73530000000000000000000000000000000000000d5af43d82803e903d91602b57fd5bf3", + "storage": { + "0x079b50c9ea8b3523e07d83295bfee8283b220c82526d7e046695778db0093d05": "0x00000000000000000000000000000000000000000000000000000000002d4c65", + "0x660c1bc2d47d7d811c3cf3324a889eebebd8edca5e38a70b3ba2905b38e251d3": "0x000000000000000000000000000000000000000000000000000000000011c927" + } + }, + "0x530000000000000000000000000000000000000d": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x608060405234801561000f575f80fd5b506004361061016e575f3560e01c806370a08231116100d25780639dc29fac11610088578063c820f14611610063578063c820f14614610354578063d505accf14610367578063dd62ed3e1461037a575f80fd5b80639dc29fac1461031b578063a457c2d71461032e578063a9059cbb14610341575f80fd5b80637ecebe00116100b85780637ecebe00146102e557806384b0196e146102f857806395d89b4114610313575f80fd5b806370a0823114610290578063797594b0146102c5575f80fd5b8063313ce56711610127578063395093511161010d57806339509351146102555780634000aea01461026857806340c10f191461027b575f80fd5b8063313ce5671461021d5780633644e5151461024d575f80fd5b8063116191b611610157578063116191b6146101b357806318160ddd146101f857806323b872dd1461020a575f80fd5b806306fdde0314610172578063095ea7b314610190575b5f80fd5b61017a6103bf565b60405161018791906119ce565b60405180910390f35b6101a361019e366004611a0f565b61044f565b6040519015158152602001610187565b60cc546101d39073ffffffffffffffffffffffffffffffffffffffff1681565b60405173ffffffffffffffffffffffffffffffffffffffff9091168152602001610187565b6035545b604051908152602001610187565b6101a3610218366004611a37565b610468565b60cd5474010000000000000000000000000000000000000000900460ff1660405160ff9091168152602001610187565b6101fc61048b565b6101a3610263366004611a0f565b610499565b6101a3610276366004611a70565b6104e4565b61028e610289366004611a0f565b61054d565b005b6101fc61029e366004611af0565b73ffffffffffffffffffffffffffffffffffffffff165f9081526033602052604090205490565b60cd546101d39073ffffffffffffffffffffffffffffffffffffffff1681565b6101fc6102f3366004611af0565b6105c7565b6103006105f1565b6040516101879796959493929190611b09565b61017a6106ae565b61028e610329366004611a0f565b6106bd565b6101a361033c366004611a0f565b61072e565b6101a361034f366004611a0f565b6107e4565b61028e610362366004611cad565b6107f1565b61028e610375366004611d3d565b610a08565b6101fc610388366004611da2565b73ffffffffffffffffffffffffffffffffffffffff9182165f90815260346020908152604080832093909416825291909152205490565b6060603680546103ce90611dd3565b80601f01602080910402602001604051908101604052809291908181526020018280546103fa90611dd3565b80156104455780601f1061041c57610100808354040283529160200191610445565b820191905f5260205f20905b81548152906001019060200180831161042857829003601f168201915b5050505050905090565b5f3361045c818585610b90565b60019150505b92915050565b5f33610475858285610d0f565b610480858585610dcb565b506001949350505050565b5f610494610ff1565b905090565b335f81815260346020908152604080832073ffffffffffffffffffffffffffffffffffffffff8716845290915281205490919061045c90829086906104df908790611e1e565b610b90565b5f6104ef85856107e4565b5073ffffffffffffffffffffffffffffffffffffffff85163b1561048057610480858585858080601f0160208091040260200160405190810160405280939291908181526020018383808284375f92019190915250610ffa92505050565b60cc5473ffffffffffffffffffffffffffffffffffffffff1633146105b95760405162461bcd60e51b815260206004820152600c60248201527f4f6e6c792047617465776179000000000000000000000000000000000000000060448201526064015b60405180910390fd5b6105c38282611085565b5050565b73ffffffffffffffffffffffffffffffffffffffff81165f90815260996020526040812054610462565b5f6060805f805f60606065545f801b14801561060d5750606654155b6106595760405162461bcd60e51b815260206004820152601560248201527f4549503731323a20556e696e697469616c697a6564000000000000000000000060448201526064016105b0565b61066161115e565b61066961116d565b604080515f808252602082019092527f0f000000000000000000000000000000000000000000000000000000000000009b939a50919850469750309650945092509050565b6060603780546103ce90611dd3565b60cc5473ffffffffffffffffffffffffffffffffffffffff1633146107245760405162461bcd60e51b815260206004820152600c60248201527f4f6e6c792047617465776179000000000000000000000000000000000000000060448201526064016105b0565b6105c3828261117c565b335f81815260346020908152604080832073ffffffffffffffffffffffffffffffffffffffff87168452909152812054909190838110156107d75760405162461bcd60e51b815260206004820152602560248201527f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f7760448201527f207a65726f00000000000000000000000000000000000000000000000000000060648201526084016105b0565b6104808286868403610b90565b5f3361045c818585610dcb565b5f54610100900460ff161580801561080f57505f54600160ff909116105b806108285750303b15801561082857505f5460ff166001145b61089a5760405162461bcd60e51b815260206004820152602e60248201527f496e697469616c697a61626c653a20636f6e747261637420697320616c72656160448201527f647920696e697469616c697a656400000000000000000000000000000000000060648201526084016105b0565b5f80547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016600117905580156108f6575f80547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff166101001790555b6108ff86611309565b61090986866113c7565b60cd805460cc805473ffffffffffffffffffffffffffffffffffffffff8088167fffffffffffffffffffffffff00000000000000000000000000000000000000009283161790925590851660ff88167401000000000000000000000000000000000000000002919091167fffffffffffffffffffffff000000000000000000000000000000000000000000909216919091171790558015610a00575f80547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff169055604051600181527f7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb38474024989060200160405180910390a15b505050505050565b83421115610a585760405162461bcd60e51b815260206004820152601d60248201527f45524332305065726d69743a206578706972656420646561646c696e6500000060448201526064016105b0565b5f7f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9888888610a868c61144d565b60408051602081019690965273ffffffffffffffffffffffffffffffffffffffff94851690860152929091166060840152608083015260a082015260c0810186905260e0016040516020818303038152906040528051906020012090505f610aed82611481565b90505f610afc828787876114c8565b90508973ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1614610b795760405162461bcd60e51b815260206004820152601e60248201527f45524332305065726d69743a20696e76616c6964207369676e6174757265000060448201526064016105b0565b610b848a8a8a610b90565b50505050505050505050565b73ffffffffffffffffffffffffffffffffffffffff8316610c185760405162461bcd60e51b8152602060048201526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f2061646460448201527f726573730000000000000000000000000000000000000000000000000000000060648201526084016105b0565b73ffffffffffffffffffffffffffffffffffffffff8216610ca15760405162461bcd60e51b815260206004820152602260248201527f45524332303a20617070726f766520746f20746865207a65726f20616464726560448201527f737300000000000000000000000000000000000000000000000000000000000060648201526084016105b0565b73ffffffffffffffffffffffffffffffffffffffff8381165f8181526034602090815260408083209487168084529482529182902085905590518481527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92591015b60405180910390a3505050565b73ffffffffffffffffffffffffffffffffffffffff8381165f908152603460209081526040808320938616835292905220547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114610dc55781811015610db85760405162461bcd60e51b815260206004820152601d60248201527f45524332303a20696e73756666696369656e7420616c6c6f77616e636500000060448201526064016105b0565b610dc58484848403610b90565b50505050565b73ffffffffffffffffffffffffffffffffffffffff8316610e545760405162461bcd60e51b815260206004820152602560248201527f45524332303a207472616e736665722066726f6d20746865207a65726f20616460448201527f647265737300000000000000000000000000000000000000000000000000000060648201526084016105b0565b73ffffffffffffffffffffffffffffffffffffffff8216610edd5760405162461bcd60e51b815260206004820152602360248201527f45524332303a207472616e7366657220746f20746865207a65726f206164647260448201527f657373000000000000000000000000000000000000000000000000000000000060648201526084016105b0565b73ffffffffffffffffffffffffffffffffffffffff83165f9081526033602052604090205481811015610f785760405162461bcd60e51b815260206004820152602660248201527f45524332303a207472616e7366657220616d6f756e742065786365656473206260448201527f616c616e6365000000000000000000000000000000000000000000000000000060648201526084016105b0565b73ffffffffffffffffffffffffffffffffffffffff8085165f8181526033602052604080822086860390559286168082529083902080548601905591517fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef90610fe49086815260200190565b60405180910390a3610dc5565b5f6104946114ee565b6040517fa4c0ed36000000000000000000000000000000000000000000000000000000008152839073ffffffffffffffffffffffffffffffffffffffff82169063a4c0ed369061105290339087908790600401611e56565b5f604051808303815f87803b158015611069575f80fd5b505af115801561107b573d5f803e3d5ffd5b5050505050505050565b73ffffffffffffffffffffffffffffffffffffffff82166110e85760405162461bcd60e51b815260206004820152601f60248201527f45524332303a206d696e7420746f20746865207a65726f20616464726573730060448201526064016105b0565b8060355f8282546110f99190611e1e565b909155505073ffffffffffffffffffffffffffffffffffffffff82165f818152603360209081526040808320805486019055518481527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a35050565b6060606780546103ce90611dd3565b6060606880546103ce90611dd3565b73ffffffffffffffffffffffffffffffffffffffff82166112055760405162461bcd60e51b815260206004820152602160248201527f45524332303a206275726e2066726f6d20746865207a65726f2061646472657360448201527f730000000000000000000000000000000000000000000000000000000000000060648201526084016105b0565b73ffffffffffffffffffffffffffffffffffffffff82165f90815260336020526040902054818110156112a05760405162461bcd60e51b815260206004820152602260248201527f45524332303a206275726e20616d6f756e7420657863656564732062616c616e60448201527f636500000000000000000000000000000000000000000000000000000000000060648201526084016105b0565b73ffffffffffffffffffffffffffffffffffffffff83165f8181526033602090815260408083208686039055603580548790039055518581529192917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9101610d02565b505050565b5f54610100900460ff166113855760405162461bcd60e51b815260206004820152602b60248201527f496e697469616c697a61626c653a20636f6e7472616374206973206e6f74206960448201527f6e697469616c697a696e6700000000000000000000000000000000000000000060648201526084016105b0565b6113c4816040518060400160405280600181526020017f3100000000000000000000000000000000000000000000000000000000000000815250611561565b50565b5f54610100900460ff166114435760405162461bcd60e51b815260206004820152602b60248201527f496e697469616c697a61626c653a20636f6e7472616374206973206e6f74206960448201527f6e697469616c697a696e6700000000000000000000000000000000000000000060648201526084016105b0565b6105c38282611604565b73ffffffffffffffffffffffffffffffffffffffff81165f9081526099602052604090208054600181018255905b50919050565b5f61046261148d610ff1565b836040517f19010000000000000000000000000000000000000000000000000000000000008152600281019290925260228201526042902090565b5f805f6114d787878787611699565b915091506114e481611781565b5095945050505050565b5f7f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f6115186118e5565b61152061193d565b60408051602081019490945283019190915260608201524660808201523060a082015260c00160405160208183030381529060405280519060200120905090565b5f54610100900460ff166115dd5760405162461bcd60e51b815260206004820152602b60248201527f496e697469616c697a61626c653a20636f6e7472616374206973206e6f74206960448201527f6e697469616c697a696e6700000000000000000000000000000000000000000060648201526084016105b0565b60676115e98382611ede565b5060686115f68282611ede565b50505f606581905560665550565b5f54610100900460ff166116805760405162461bcd60e51b815260206004820152602b60248201527f496e697469616c697a61626c653a20636f6e7472616374206973206e6f74206960448201527f6e697469616c697a696e6700000000000000000000000000000000000000000060648201526084016105b0565b603661168c8382611ede565b5060376113048282611ede565b5f807f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08311156116ce57505f90506003611778565b604080515f8082526020820180845289905260ff881692820192909252606081018690526080810185905260019060a0016020604051602081039080840390855afa15801561171f573d5f803e3d5ffd5b50506040517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0015191505073ffffffffffffffffffffffffffffffffffffffff8116611772575f60019250925050611778565b91505f90505b94509492505050565b5f81600481111561179457611794611ff6565b0361179c5750565b60018160048111156117b0576117b0611ff6565b036117fd5760405162461bcd60e51b815260206004820152601860248201527f45434453413a20696e76616c6964207369676e6174757265000000000000000060448201526064016105b0565b600281600481111561181157611811611ff6565b0361185e5760405162461bcd60e51b815260206004820152601f60248201527f45434453413a20696e76616c6964207369676e6174757265206c656e6774680060448201526064016105b0565b600381600481111561187257611872611ff6565b036113c45760405162461bcd60e51b815260206004820152602260248201527f45434453413a20696e76616c6964207369676e6174757265202773272076616c60448201527f756500000000000000000000000000000000000000000000000000000000000060648201526084016105b0565b5f806118ef61115e565b805190915015611906578051602090910120919050565b60655480156119155792915050565b7fc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a4709250505090565b5f8061194761116d565b80519091501561195e578051602090910120919050565b60665480156119155792915050565b5f81518084525f5b8181101561199157602081850181015186830182015201611975565b505f6020828601015260207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f83011685010191505092915050565b602081525f6119e0602083018461196d565b9392505050565b803573ffffffffffffffffffffffffffffffffffffffff81168114611a0a575f80fd5b919050565b5f8060408385031215611a20575f80fd5b611a29836119e7565b946020939093013593505050565b5f805f60608486031215611a49575f80fd5b611a52846119e7565b9250611a60602085016119e7565b9150604084013590509250925092565b5f805f8060608587031215611a83575f80fd5b611a8c856119e7565b935060208501359250604085013567ffffffffffffffff80821115611aaf575f80fd5b818701915087601f830112611ac2575f80fd5b813581811115611ad0575f80fd5b886020828501011115611ae1575f80fd5b95989497505060200194505050565b5f60208284031215611b00575f80fd5b6119e0826119e7565b7fff00000000000000000000000000000000000000000000000000000000000000881681525f602060e06020840152611b4560e084018a61196d565b8381036040850152611b57818a61196d565b6060850189905273ffffffffffffffffffffffffffffffffffffffff8816608086015260a0850187905284810360c0860152855180825260208088019350909101905f5b81811015611bb757835183529284019291840191600101611b9b565b50909c9b505050505050505050505050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b5f82601f830112611c05575f80fd5b813567ffffffffffffffff80821115611c2057611c20611bc9565b604051601f83017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0908116603f01168101908282118183101715611c6657611c66611bc9565b81604052838152866020858801011115611c7e575f80fd5b836020870160208301375f602085830101528094505050505092915050565b803560ff81168114611a0a575f80fd5b5f805f805f60a08688031215611cc1575f80fd5b853567ffffffffffffffff80821115611cd8575f80fd5b611ce489838a01611bf6565b96506020880135915080821115611cf9575f80fd5b50611d0688828901611bf6565b945050611d1560408701611c9d565b9250611d23606087016119e7565b9150611d31608087016119e7565b90509295509295909350565b5f805f805f805f60e0888a031215611d53575f80fd5b611d5c886119e7565b9650611d6a602089016119e7565b95506040880135945060608801359350611d8660808901611c9d565b925060a0880135915060c0880135905092959891949750929550565b5f8060408385031215611db3575f80fd5b611dbc836119e7565b9150611dca602084016119e7565b90509250929050565b600181811c90821680611de757607f821691505b60208210810361147b577f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b80820180821115610462577f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b73ffffffffffffffffffffffffffffffffffffffff84168152826020820152606060408201525f611e8a606083018461196d565b95945050505050565b601f82111561130457805f5260205f20601f840160051c81016020851015611eb85750805b601f840160051c820191505b81811015611ed7575f8155600101611ec4565b5050505050565b815167ffffffffffffffff811115611ef857611ef8611bc9565b611f0c81611f068454611dd3565b84611e93565b602080601f831160018114611f5e575f8415611f285750858301515b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600386901b1c1916600185901b178555610a00565b5f858152602081207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08616915b82811015611faa57888601518255948401946001909101908401611f8b565b5085821015611fe657878501517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600388901b60f8161c191681555b5050505050600190811b01905550565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602160045260245ffdfea164736f6c6343000818000a", + "storage": {} + }, + "0x5300000000000000000000000000000000000021": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": { + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706d": "0x000000000000000000000000c7d67a9cbb121b3b0b9c053dd9f469523243379a", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706e": "0x0000000000000000000000000000000000000000000000000000000000000034", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706f": "0x0000000000000000000000000000000000000000000000000000000000000601", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e6377070": "0x00000000000000000000000000000000000000000000000000000000000f4240", + "0xbb86fbc034f4e382929974bcd8419ed626b0ea647f962d89ba2fb6bd28785ab9": "0x00000000000000000000000000000000000000000000000000017969711b4fae" + } + }, + "0x530000000000000000000000000000000000000f": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0x000000000000000000000000f4a82c4ab398771c6c7a0c0dedf0b34fc0161192", + "0x0000000000000000000000000000000000000000000000000000000000000001": "0x000000000000000000000000000000000000000000000000000000000c669869", + "0x0000000000000000000000000000000000000000000000000000000000000002": "0x00000000000000000000000000000000000000000000000000000000000009c4", + "0x0000000000000000000000000000000000000000000000000000000000000003": "0x000000000000000000000000000000000000000000000000000000003b9aca00", + "0x0000000000000000000000000000000000000000000000000000000000000004": "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000000000000000000000000000006": "0x00000000000000000000000000000000000000000000000000000000005cc6c6", + "0x0000000000000000000000000000000000000000000000000000000000000007": "0x000000000000000000000000000000000000000000000000000001e6a77be7ac", + "0x0000000000000000000000000000000000000000000000000000000000000008": "0x0000000000000000000000000000000000000000000000000000000034faa1da", + "0x0000000000000000000000000000000000000000000000000000000000000009": "0x0000000000000000000000000000000000000000000000000000000000000001" + } + } + }, + "transaction": { + "type": "0x7f", + "version": "0x0", + "feeTokenID": "0x1", + "feeLimit": "0x0", + "nonce": "0x1", + "gasPrice": "0xf4240", + "gasLimit": [ + "0x181b1" + ], + "to": "0xc7d67a9cbb121b3b0b9c053dd9f469523243379a", + "value": [ + "0x0" + ], + "data": [ + "0xa9059cbb0000000000000000000000007ef77f9a8cbf84d1becf98b7768d49c7b4fef6ed00000000000000000000000000000000000000000000000000000000002d2a80" + ], + "secretKey": "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" + }, + "post": { + "Jade": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0x78bee0ec231c3fc7b87ebd7ec48c3d3c61a35338749c8727315fd2c8507daaf1", + "logs": "0x735e75fcb5f5511a4bf02e5f79e7b123630ef4b1b7208a71bf2eaf451221bd59" + } + ] + } + } +} diff --git a/crates/evm/src/block/factory.rs b/crates/evm/src/block/factory.rs index 0ba66897..a1219465 100644 --- a/crates/evm/src/block/factory.rs +++ b/crates/evm/src/block/factory.rs @@ -74,6 +74,6 @@ impl BlockExecutorFactory for MorphBlockExecutorFactory { DB: StateDB, I: Inspector>, { - MorphBlockExecutor::new(evm, self.spec.clone(), self.receipt_builder) + MorphBlockExecutor::new(evm, self.receipt_builder) } } diff --git a/crates/evm/src/block/mod.rs b/crates/evm/src/block/mod.rs index bb032056..fbdecf02 100644 --- a/crates/evm/src/block/mod.rs +++ b/crates/evm/src/block/mod.rs @@ -23,7 +23,6 @@ use alloy_evm::{ }, }; use alloy_primitives::{Address, Log, U256}; -use morph_chainspec::{MorphChainSpec, MorphHardfork, MorphHardforks}; use morph_primitives::{MorphReceipt, MorphTxEnvelope}; use morph_revm::{L1_GAS_PRICE_ORACLE_ADDRESS, MorphHaltReason, TokenFeeInfo, evm::MorphContext}; use reth_primitives_traits::Recovered; @@ -84,8 +83,6 @@ impl TxResult for MorphTxResult { pub struct MorphBlockExecutor { /// The EVM used by executor (owned, not a reference) evm: MorphEvm, - /// Chain specification - spec: std::sync::Arc, /// Receipt builder receipt_builder: DefaultMorphReceiptBuilder, /// Receipts of executed transactions @@ -97,9 +94,6 @@ pub struct MorphBlockExecutor { /// Unlike receipt gas, L1 messages reserve their full gas limit because Morph geth does not /// return their unused gas to the block gas pool. gas_pool_used: u64, - /// Cached hardfork for this block (constant across all transactions). - /// Set in `apply_pre_execution_changes`, reused in `commit_transaction`. - hardfork: MorphHardfork, } impl MorphBlockExecutor @@ -111,21 +105,14 @@ where /// /// # Arguments /// * `evm` - The EVM instance configured for Morph execution - /// * `spec` - Chain specification containing hardfork information /// * `receipt_builder` - Builder for constructing transaction receipts - pub(crate) fn new( - evm: MorphEvm, - spec: std::sync::Arc, - receipt_builder: DefaultMorphReceiptBuilder, - ) -> Self { + pub(crate) fn new(evm: MorphEvm, receipt_builder: DefaultMorphReceiptBuilder) -> Self { Self { evm, - spec, receipt_builder, receipts: Vec::new(), gas_used: 0, gas_pool_used: 0, - hardfork: MorphHardfork::default(), } } @@ -138,7 +125,6 @@ where &mut self, tx: &MorphTxEnvelope, sender: Address, - hardfork: MorphHardfork, ) -> Result, BlockExecutionError> { if !tx.is_morph_tx() { return Ok(None); @@ -169,12 +155,13 @@ where let token_info = match self.evm.cached_token_fee_info() { Some(info) => Some(info), - None => { - TokenFeeInfo::load_for_caller(self.evm.db_mut(), fee_token_id, sender, hardfork) - .map_err(|e| { - BlockExecutionError::msg(format!("Failed to fetch token fee info: {e:?}")) - })? - } + // Only `price_ratio` and `scale` are read below, and both come straight from + // registry storage. `load_storage_only` reads exactly that and never builds a + // temporary EVM to resolve a balance this receipt has no use for. + None => TokenFeeInfo::load_storage_only(self.evm.db_mut(), fee_token_id, sender) + .map_err(|e| { + BlockExecutionError::msg(format!("Failed to fetch token fee info: {e:?}")) + })?, }; Ok(token_info.map(|info| MorphReceiptTxFields { @@ -218,12 +205,6 @@ where .basic(L1_GAS_PRICE_ORACLE_ADDRESS) .map_err(BlockExecutionError::other)?; - let block_number: u64 = self.evm.block().number.to(); - let hardfork = self - .spec - .morph_hardfork_at(block_number, self.evm.block().timestamp.to::()); - self.hardfork = hardfork; - Ok(()) } @@ -300,7 +281,7 @@ where // are tracing-only — the trait API no longer permits us to surface errors // from `commit_transaction`. let (tx, signer) = recovered.into_parts(); - let morph_tx_fields = match self.get_morph_tx_fields(&tx, signer, self.hardfork) { + let morph_tx_fields = match self.get_morph_tx_fields(&tx, signer) { Ok(fields) => fields, Err(err) => { tracing::error!( diff --git a/crates/evm/src/block/receipt.rs b/crates/evm/src/block/receipt.rs index 347c8265..4d791b3d 100644 --- a/crates/evm/src/block/receipt.rs +++ b/crates/evm/src/block/receipt.rs @@ -161,8 +161,12 @@ impl MorphReceiptBuilder for DefaultMorphReceiptBuilder { // Assemble logs in chronological order matching go-ethereum: // [deduct Transfer] + [main tx logs] + [refund Transfer] - // Fee logs are cached separately from the journal so they survive - // main tx revert (revm's ExecutionResult::Revert carries no logs). + // The fee logs cannot come from `result`. The call-mode deduction runs a + // mid-transaction `finalize()` that clears the journal's logs, so the handler + // moves them out first, and it drains the refund's logs the same way. `result` + // carries only the main frame's logs, which a revert has already discarded, + // while the fee logs survive it as they do in go-ethereum, whose `StateDB.logs` + // sit outside the snapshot/revert mechanism. let is_success = result.is_success(); let main_logs = result.into_logs(); let mut logs = diff --git a/crates/evm/src/evm.rs b/crates/evm/src/evm.rs index e2454e4d..368f605d 100644 --- a/crates/evm/src/evm.rs +++ b/crates/evm/src/evm.rs @@ -13,7 +13,6 @@ use alloy_evm::{ use alloy_primitives::{Address, Bytes}; use morph_chainspec::hardfork::MorphHardfork; use morph_revm::{MorphHaltReason, MorphInvalidTransaction, MorphTxEnv, evm::MorphContext}; -use reth_revm::MainContext; use std::ops::{Deref, DerefMut}; use crate::MorphBlockEnv; @@ -66,17 +65,7 @@ pub struct MorphEvm { impl MorphEvm { /// Create a new [`MorphEvm`] instance. pub fn new(db: DB, input: EvmEnv) -> Self { - let ctx = Context::mainnet() - .with_db(db) - .with_block(input.block_env) - .with_cfg(input.cfg_env) - .with_tx(Default::default()) - .with_chain(morph_revm::l1block::L1BlockInfo::default()); - - // Build the inner MorphEvm which creates precompiles once. - // Derive the PrecompilesMap from the inner's precompiles to avoid - // a second MorphPrecompiles::new_with_spec call. - let inner = morph_revm::MorphEvm::new(ctx, NoOpInspector {}); + let inner = morph_revm::MorphEvm::from_env(db, input, NoOpInspector {}); let precompiles_map = PrecompilesMap::from_static(inner.precompiles.precompiles()); Self { diff --git a/crates/node/src/test_utils.rs b/crates/node/src/test_utils.rs index b61b5809..24d22560 100644 --- a/crates/node/src/test_utils.rs +++ b/crates/node/src/test_utils.rs @@ -828,12 +828,33 @@ impl L1MessageBuilder { /// - token_address = `TEST_TOKEN_ADDRESS` /// - price_ratio = 1e18 (1:1 with ETH) /// - decimals = 18, isActive = true +/// - balanceSlot = 0, i.e. EVM-call mode +/// +/// Call mode is what mainnet runs: every registered fee token there has its +/// `balanceSlot` cleared, so the fee is moved by real `balanceOf` / `transfer` +/// calls into the token contract and the receipt carries their `Transfer` events. pub const TEST_TOKEN_ID: u16 = 1; /// Address of the test ERC20 token deployed in the test genesis. /// /// Pre-funded with 1000 tokens (1e21 wei) for test accounts 0 and 1. /// Address: `0x5300000000000000000000000000000000000022` +/// +/// The genesis gives it the optimized runtime of: +/// +/// ```solidity +/// contract Slot1Token { +/// uint256 private dummy; +/// mapping(address => uint256) public balanceOf; // slot 1 +/// event Transfer(address indexed from, address indexed to, uint256 value); +/// function transfer(address to, uint256 amount) external returns (bool) { ... } +/// } +/// ``` +/// +/// Real code is what makes the registry's EVM-call mode usable: the fee path calls +/// `balanceOf` and `transfer` on this contract rather than writing its storage +/// directly. Keeping `balanceOf` at slot 1 also lets [`test_token_balance_slot`] +/// derive the same slot independently as a test oracle. pub const TEST_TOKEN_ADDRESS: Address = Address::new([ 0x53, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, @@ -849,9 +870,9 @@ pub const TEST_FEE_VAULT_ADDRESS: Address = Address::new([ /// Base slot of the test token's `balances` mapping. /// -/// The registry stores this one-based so that zero means "unknown", and -/// `morph_revm`'s token-fee reader subtracts one. The test genesis registers `2` -/// for `TEST_TOKEN_ID`, so the effective base slot is `1`. +/// The registry's own `balanceSlot` is zero (call mode), so this is not read from +/// the registry — it mirrors the layout of the token contract's bytecode so tests +/// can check balances without going through the fee-token code under test. const TEST_TOKEN_BALANCE_BASE_SLOT: u64 = 1; /// Storage slot holding `account`'s balance of the test ERC20 token. diff --git a/crates/node/tests/assets/test-genesis.json b/crates/node/tests/assets/test-genesis.json index 09cc751b..c23a20b8 100644 --- a/crates/node/tests/assets/test-genesis.json +++ b/crates/node/tests/assets/test-genesis.json @@ -67,7 +67,7 @@ "code": "0x00", "storage": { "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706d": "0x0000000000000000000000005300000000000000000000000000000000000022", - "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706e": "0x0000000000000000000000000000000000000000000000000000000000000002", + "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706e": "0x0000000000000000000000000000000000000000000000000000000000000000", "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e637706f": "0x0000000000000000000000000000000000000000000000000000000000001201", "0x53bdca72fa8d2e145a1b3bd11cde5bd75428acd18eac3d6adf4e06e7e6377070": "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000", "0xbb86fbc034f4e382929974bcd8419ed626b0ea647f962d89ba2fb6bd28785ab9": "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" @@ -75,7 +75,7 @@ }, "0x5300000000000000000000000000000000000022": { "balance": "0x0", - "code": "0x00", + "code": "0x608060405234801561000f575f5ffd5b5060043610610034575f3560e01c806370a0823114610038578063a9059cbb1461006a575b5f5ffd5b61005761004636600461015e565b60016020525f908152604090205481565b6040519081526020015b60405180910390f35b61007d61007836600461017e565b61008d565b6040519015158152602001610061565b335f90815260016020526040812054828110156100da5760405162461bcd60e51b815260206004820152600760248201526662616c616e636560c81b604482015260640160405180910390fd5b335f81815260016020908152604080832087860390556001600160a01b03881680845292819020805488019055518681529192917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a35060019392505050565b80356001600160a01b0381168114610159575f5ffd5b919050565b5f6020828403121561016e575f5ffd5b61017782610143565b9392505050565b5f5f6040838503121561018f575f5ffd5b61019883610143565b94602093909301359350505056", "storage": { "0xa3c1274aadd82e4d12c8004c33fb244ca686dad4fcc8957fc5668588c11d9502": "0x00000000000000000000000000000000000000000000003635c9adc5dea00000", "0x3c8e904cdb19937d60d41c8d984b1a8803ad6e0891b4f9e032dcec2a22c2c7f5": "0x00000000000000000000000000000000000000000000003635c9adc5dea00000" diff --git a/crates/node/tests/it/morph_tx.rs b/crates/node/tests/it/morph_tx.rs index 97cef34d..7fe7d58d 100644 --- a/crates/node/tests/it/morph_tx.rs +++ b/crates/node/tests/it/morph_tx.rs @@ -376,21 +376,6 @@ fn address_topic(address: Address) -> B256 { B256::from(topic) } -/// Optimized runtime for: -/// -/// ```solidity -/// contract Slot1Token { -/// uint256 private dummy; -/// mapping(address => uint256) public balanceOf; // slot 1 -/// event Transfer(address indexed from, address indexed to, uint256 value); -/// function transfer(address to, uint256 amount) external returns (bool) { ... } -/// } -/// ``` -/// -/// Keeping `balanceOf` at slot 1 lets the test token use the same storage layout -/// as `tests/assets/test-genesis.json` and the token registry's direct-slot path. -const SLOT1_ERC20_RUNTIME_CODE: &str = "0x608060405234801561000f575f5ffd5b5060043610610034575f3560e01c806370a0823114610038578063a9059cbb1461006a575b5f5ffd5b61005761004636600461015e565b60016020525f908152604090205481565b6040519081526020015b60405180910390f35b61007d61007836600461017e565b61008d565b6040519015158152602001610061565b335f90815260016020526040812054828110156100da5760405162461bcd60e51b815260206004820152600760248201526662616c616e636560c81b604482015260640160405180910390fd5b335f81815260016020908152604080832087860390556001600160a01b03881680845292819020805488019055518681529192917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a35060019392505050565b80356001600160a01b0381168114610159575f5ffd5b919050565b5f6020828403121561016e575f5ffd5b61017782610143565b9392505050565b5f5f6040838503121561018f575f5ffd5b61019883610143565b94602093909301359350505056"; - /// After a successful MorphTx v0 with ERC20 fee, the sender's token balance /// must decrease (fee was charged from tokens, not ETH). #[tokio::test(flavor = "multi_thread")] @@ -492,7 +477,6 @@ async fn token_fee_transfer_gas_regression( let token_addr = morph_node::test_utils::TEST_TOKEN_ADDRESS; let (mut nodes, wallet) = TestNodeBuilder::new() .with_schedule(schedule) - .with_account_code(token_addr, SLOT1_ERC20_RUNTIME_CODE) .build() .await?; let mut node = nodes.pop().unwrap(); @@ -560,17 +544,24 @@ async fn token_fee_transfer_gas_regression( .iter() .filter(|log| log.address == token_addr && log.topics().first() == Some(&transfer_topic)) .collect(); + // In call mode the fee is moved by real ERC20 calls, so the transaction's own + // transfer arrives bracketed by them, in go-ethereum's order: deduction, main, + // reimbursement. assert_eq!( transfer_logs.len(), - 1, - "the main ERC20 transfer should execute against the fee token contract" + 3, + "receipt should carry the fee deduction, the main transfer and the fee refund" ); assert_eq!(transfer_logs[0].topics()[1], address_topic(sender)); - assert_eq!(transfer_logs[0].topics()[2], address_topic(recipient)); + assert_eq!(transfer_logs[0].topics()[2], address_topic(fee_vault)); + assert_eq!(transfer_logs[1].topics()[1], address_topic(sender)); + assert_eq!(transfer_logs[1].topics()[2], address_topic(recipient)); assert_eq!( - transfer_logs[0].data.data.as_ref(), + transfer_logs[1].data.data.as_ref(), amount.to_be_bytes::<32>() ); + assert_eq!(transfer_logs[2].topics()[1], address_topic(fee_vault)); + assert_eq!(transfer_logs[2].topics()[2], address_topic(sender)); let state_after = node.inner.provider.latest()?; let sender_after = state_after @@ -640,10 +631,21 @@ const RUNTIME_REVERT_INIT: &[u8] = &[ /// 1. Block 1: Deploy a contract whose runtime always reverts (EIP-1559 tx) /// 2. Block 2: Call that contract with MorphTx v0 (ERC20 fee) /// 3. Verify: receipt.status = false, but token balance decreased +/// 4. Verify: the receipt still carries both fee `Transfer` events /// /// This exercises the handler's `validate_and_deduct_token_fee` (charges fee /// upfront) and `reimburse_caller_token_fee` (partial refund for unused gas) /// paths when the main transaction execution reverts. +/// +/// The log assertion is the point of running the fee path on a *reverting* main +/// frame. go-ethereum keeps `StateDB.logs` outside the state snapshot/revert +/// mechanism, so the deduction's `Transfer` survives a main-frame revert; that +/// is the entire reason morph-reth caches fee logs in `pre_fee_logs` / +/// `post_fee_logs` instead of leaving them in the journal (`crates/evm/src/block/receipt.rs`). +/// A regression there -- the fee logs dropped, or restored into the reverted +/// frame -- changes the receipt's logs and therefore the block's receipts root, +/// and no state assertion in this test would notice. This is the only test that +/// runs the production receipt builder against a reverting main frame. #[tokio::test(flavor = "multi_thread")] async fn morph_tx_v0_token_fee_still_charged_on_revert() -> eyre::Result<()> { reth_tracing::init_test_tracing(); @@ -720,6 +722,40 @@ async fn morph_tx_v0_token_fee_still_charged_on_revert() -> eyre::Result<()> { before={bal_before}, after={bal_after}" ); + // Both fee transfers must survive the main frame's revert: go-ethereum keeps + // `StateDB.logs` outside the state snapshot/revert mechanism, so the deduction's + // `Transfer` is still in the receipt while the reverted main frame contributes + // none. Order is go-ethereum's: deduction, (empty) main frame, reimbursement. + let fee_vault = morph_node::test_utils::TEST_FEE_VAULT_ADDRESS; + let transfer_topic = erc20_transfer_topic(); + let transfer_logs: Vec<_> = receipt + .logs() + .iter() + .filter(|log| log.address == token_addr && log.topics().first() == Some(&transfer_topic)) + .collect(); + assert_eq!( + transfer_logs.len(), + 2, + "receipt must carry the fee deduction and the fee reimbursement even though \ + the main frame reverted; dropping the deduction's log changes the receipts root. \ + got {transfer_logs:?}" + ); + assert_eq!( + (transfer_logs[0].topics()[1], transfer_logs[0].topics()[2]), + (address_topic(sender), address_topic(fee_vault)), + "first log must be the fee deduction (sender -> fee vault)" + ); + assert_ne!( + transfer_logs[0].data.data.as_ref(), + [0u8; 32], + "the deduction must move a non-zero fee" + ); + assert_eq!( + (transfer_logs[1].topics()[1], transfer_logs[1].topics()[2]), + (address_topic(fee_vault), address_topic(sender)), + "second log must be the fee reimbursement (fee vault -> sender)" + ); + // The receipt should carry MorphTx-specific fee fields match &receipt { morph_primitives::MorphReceipt::Morph(morph_receipt) => { diff --git a/crates/revm/src/error.rs b/crates/revm/src/error.rs index 4515a2ae..8ed377e6 100644 --- a/crates/revm/src/error.rs +++ b/crates/revm/src/error.rs @@ -27,6 +27,11 @@ pub enum MorphInvalidTransaction { #[error("Token with ID {0} has invalid fee configuration")] InvalidTokenConfig(u16), + /// The token balance call reverted, violated static execution, or returned malformed data. + #[error("Token balance query failed")] + TokenBalanceQueryFailed, + + /// The transfer failed or its return value/balance delta was invalid. #[error("Token transfer failed: {reason}")] TokenTransferFailed { /// Token transfer failure reason. diff --git a/crates/revm/src/evm.rs b/crates/revm/src/evm.rs index a0969696..eda44e68 100644 --- a/crates/revm/src/evm.rs +++ b/crates/revm/src/evm.rs @@ -100,6 +100,9 @@ pub struct MorphEvm { /// receipt-building path (the handler already has the encoded bytes via /// `MorphTxEnv.rlp_bytes`). pub(crate) cached_l1_data_fee: U256, + /// Signed refund counter from a successful fee deduction call. + /// Applied before the final refund cap; refund-transfer refunds are excluded. + pub(crate) pre_fee_refund: i64, /// Transfer event logs from token fee deduction (pre-execution phase). /// /// In go-ethereum, `buyAltTokenGas()` emits Transfer events into `StateDB.logs` @@ -113,6 +116,14 @@ pub struct MorphEvm { } impl MorphEvm { + /// Constructs an EVM from the full environment used by both execution and pool queries. + pub fn from_env(db: DB, env: crate::MorphEvmEnv, inspector: I) -> Self { + let ctx = MorphContext::new(db, *env.cfg_env.spec()) + .with_cfg(env.cfg_env) + .with_block(env.block_env); + Self::new(ctx, inspector) + } + /// Create a new Morph EVM. /// /// The precompiles are automatically selected based on the hardfork spec @@ -173,6 +184,7 @@ impl MorphEvm { inner, cached_token_fee_info: None, cached_l1_data_fee: U256::ZERO, + pre_fee_refund: 0, pre_fee_logs: Vec::new(), post_fee_logs: Vec::new(), } diff --git a/crates/revm/src/handler.rs b/crates/revm/src/handler.rs index a0d9f2eb..c2b04d81 100644 --- a/crates/revm/src/handler.rs +++ b/crates/revm/src/handler.rs @@ -17,11 +17,14 @@ use revm::{ }; use crate::{ - MorphEvm, MorphInvalidTransaction, MorphTxEnv, + MorphEvm, MorphInvalidTransaction, error::MorphHaltReason, evm::MorphContext, l1block::L1BlockInfo, - token_fee::{TokenRegistryEntry, compute_mapping_slot_for_address, encode_balance_of_calldata}, + token_fee::{ + TokenFeeInfo, TokenRegistryEntry, compute_mapping_slot_for_address, + encode_balance_of_calldata, read_balance_from_storage, + }, tx::MorphTxExt, }; @@ -101,6 +104,7 @@ where ) -> Result<(), Self::Error> { // Reset per-transaction caches from the previous iteration. evm.cached_l1_data_fee = U256::ZERO; + evm.pre_fee_refund = 0; evm.cached_token_fee_info = None; evm.pre_fee_logs.clear(); evm.post_fee_logs.clear(); @@ -178,6 +182,7 @@ where exec_result.gas_mut().set_refund(0); return Ok(()); } + exec_result.gas_mut().record_refund(evm.pre_fee_refund); post_execution::refund( evm.ctx().cfg().gas_params(), exec_result.gas_mut(), @@ -422,11 +427,11 @@ where // This ensures the same price_ratio/scale is used for both deduction and reimbursement. // The cache is kept populated (not taken) so the block executor's receipt builder // can also read it without re-querying the DB. - let token_fee_info = - evm.cached_token_fee_info - .ok_or(MorphInvalidTransaction::TokenTransferFailed { - reason: "cached_token_fee_info not set by validate_and_deduct_token_fee".into(), - })?; + let token_fee_info = evm.cached_token_fee_info.ok_or_else(|| { + MorphInvalidTransaction::TokenTransferFailed { + reason: "cached_token_fee_info not set by validate_and_deduct_token_fee".into(), + } + })?; // Calculate token amount required for total fee let token_amount_required = token_fee_info.eth_to_token_amount(reimburse_eth); @@ -464,10 +469,16 @@ where .drain(log_count_before..) .collect(); evm.post_fee_logs = refund_logs; - result + result.map(|_| ()) }; if let Err(err) = refund_result { + // A contract may reject a refund, but unavailable state is not a verdict + // about the contract. Internal calls have already taken the context error, + // so it must reach the executor here rather than disappearing at finalization. + if matches!(err, EVMError::Database(_)) { + return Err(err); + } tracing::error!( target: "morph::evm", token_id = ?evm.ctx_ref().tx().fee_token_id, @@ -555,11 +566,7 @@ where let hardfork = *evm.ctx_ref().cfg().spec(); - let token_fee_info = token_registry_entry.load_for_caller( - evm.ctx_mut().journal_mut().db_mut(), - caller_addr, - hardfork, - )?; + let token_fee_info = load_token_fee_info(evm, token_registry_entry, caller_addr)?; let beneficiary = evm.ctx_ref().block().beneficiary(); let rlp_bytes = evm.ctx_ref().tx().rlp_bytes.clone().unwrap_or_default(); @@ -584,11 +591,7 @@ where // Calculate token amount required for total fee let token_amount_required = token_fee_info.eth_to_token_amount(total_eth_fee); - // Determine fee limit - let mut fee_limit = fee_limit_from_tx; - if fee_limit.is_zero() || fee_limit > token_fee_info.balance { - fee_limit = token_fee_info.balance - } + let fee_limit = token_fee_info.effective_fee_limit(fee_limit_from_tx); // Check if caller has sufficient token balance if fee_limit < token_amount_required { @@ -599,13 +602,12 @@ where .into()); } - if let Some(balance_slot) = token_fee_info.balance_slot { + if token_amount_required.is_zero() { + // Geth skips both transfer modes for a zero fee. Nonce and caches below + // still need their normal per-transaction updates. + } else if let Some(balance_slot) = token_fee_info.balance_slot { // Transfer with token slot. - // Ensure token account is loaded into the journal state, because `sload`/`sstore` - // assume the account is present. let journal = evm.ctx_mut().journal_mut(); - let _ = journal.load_account_mut(token_fee_info.token_address)?; - journal.touch(token_fee_info.token_address); let (from_storage_slot, to_storage_slot) = transfer_erc20_with_slot( journal, caller_addr, @@ -627,7 +629,7 @@ where } } else { // Transfer with evm call (from=caller, balance known from token registry). - transfer_erc20_with_evm( + evm.pre_fee_refund = transfer_erc20_with_evm( evm, caller_addr, beneficiary, @@ -635,17 +637,60 @@ where token_amount_required, Some(token_fee_info.balance), )?; + } + if token_fee_info.balance_slot.is_none() { + // balanceOf runs even for a zero fee. Geth Prepare clears its access + // list/transient storage before the main transaction in that case too. // Cache fee Transfer logs separately from the journal. // // go-ethereum's StateDB.logs is independent of the state snapshot/revert - // mechanism — fee logs survive regardless of main tx result. revm's - // ExecutionResult::Revert has no logs field, so we keep fee logs out of - // the handler pipeline entirely and merge them in the receipt builder. + // mechanism — fee logs survive regardless of main tx result. In revm they + // would not: the `finalize()` below clears the journal's logs, and whatever + // survived would still be dropped when `execution_result` commits the + // transaction. So the fee logs are kept out of the handler pipeline entirely + // and merged back in the receipt builder. evm.pre_fee_logs = std::mem::take(&mut evm.ctx_mut().journal_mut().logs); // State changes should be marked cold to avoid warm access in the main tx execution. - // finalize() clears journal state (including logs, which we already took above). + // Fee deduction ran a real EVM frame, so its state writes must survive while the + // frame's metadata must not: go-ethereum's `StateDB.Prepare` rebuilds the access + // list and resets transient storage before the main transaction + // (core/state/statedb.go:1066). `finalize()` is the nearest revm equivalent — it + // commits the deduction's state and drops the journal, undo history, logs and + // transient storage. + // + // `mark_cold` below only has to *drop* the warmth this frame's own CALL created; it + // does not restore what `Prepare` would have left warm, and must not try to. That + // warmth arrives later from upstream, which is why the two cannot be swapped: + // `run_without_catch_error` runs this deduction inside `validate()`, then + // `pre_execution()` → `pre_execution::load_accounts` re-warms the coinbase + // (EIP-3651) and the transaction's access list, and the nonce bump just below + // re-loads the caller. If a future change reorders those phases, a main frame that + // reads `COINBASE` would be charged 2600 instead of go-ethereum's 100; nothing here + // would catch it, because no fixture's main frame touches the coinbase. + // + // The `transaction_id` handling inside `finalize()` is load-bearing, not incidental. + // Warming a slot goes through `EvmStorageSlot::mark_warm_with_transaction_id`, which + // re-baselines the EIP-2200 `original_value` to the present value whenever the slot's + // transaction id differs from the journal's (revm-state/src/lib.rs). That must not + // happen to the slot the deduction just cleared: re-baselining it to zero would make + // the main frame's SSTORE a *create* (SSTORE_SET, 20000) rather than a *recreate* + // (100), and would drop the `SubRefund` that cancels the deduction frame's `+4800`. + // Measured on `main_restores_cleared_slot`: 23_291 gas becomes 38_391 (+19_900 + // -4_800), and the state root moves with the fee it implies. + // + // It does not happen because ids stay equal throughout execution. revm advances the + // id only when a transaction finishes — `commit_tx()` from `execution_result`, or + // `discard_tx()` on the error path — both after the main frame is done; + // `ExecuteEvm::finalize` then resets it to ZERO before the next transaction. So + // across this deduction and the main frame the journal's id is 0 — and this + // `finalize()` keeps it at 0 rather than advancing it. Swapping in `commit_tx()` here + // would leave the deduction-warmed slots holding 0 while the journal held 1, and the + // main frame's first touch of them would re-baseline `original_value`; the call-path + // fixtures under `bin/morph-statetest` catch exactly that. An explicit `mark_cold` + // carries no such risk: it drives only the warm/cold gas decision, never the + // re-baseline. let mut state = evm.finalize(); state.iter_mut().for_each(|(_, acc)| { acc.mark_cold(); @@ -697,57 +742,13 @@ where } } -/// Execute `f` within a journal checkpoint, saving and restoring `evm.tx`. -/// -/// On `Ok` the checkpoint is committed; on `Err` it is reverted. -/// `evm.tx` is always restored to its original value regardless of the outcome, -/// so callers of [`evm_call`] inside `f` do not need to manage `evm.tx` themselves. -#[inline] -fn with_evm_checkpoint( - evm: &mut MorphEvm, - f: impl FnOnce(&mut MorphEvm) -> Result>, -) -> Result> -where - DB: alloy_evm::Database, -{ - let tx_origin = std::mem::take(&mut evm.tx); - let checkpoint = evm.ctx_mut().journal_mut().checkpoint(); - let result = f(evm); - evm.tx = tx_origin; - match result { - Ok(val) => { - evm.ctx_mut().journal_mut().checkpoint_commit(); - Ok(val) - } - Err(err) => { - evm.ctx_mut().journal_mut().checkpoint_revert(checkpoint); - Err(err) - } - } -} - -/// Execute `f` within a journal snapshot that always reverts, saving and restoring `evm.tx`. -/// -/// This gives `f` read-only (StaticCall-like) semantics: any state changes made by -/// [`evm_call`] inside `f` are discarded when `f` returns. -#[inline] -fn with_evm_snapshot( - evm: &mut MorphEvm, - f: impl FnOnce(&mut MorphEvm) -> T, -) -> T -where - DB: alloy_evm::Database, -{ - let tx_origin = std::mem::take(&mut evm.tx); - let checkpoint = evm.ctx_mut().journal_mut().checkpoint(); - let result = f(evm); - evm.ctx_mut().journal_mut().checkpoint_revert(checkpoint); - evm.tx = tx_origin; - result -} - /// Performs an ERC20 balance transfer by directly `sload`/`sstore`-ing the token contract storage /// using the known `balance` mapping base slot, returning the computed storage slots for `from`/`to`. +/// +/// The token account is loaded and touched here, ahead of the checkpoint, rather than by the +/// callers. The journal's `sload`/`sstore` panic instead of erroring when the account is absent +/// from `journal.state`, and touching keeps the token among the transaction's state changes even +/// for a self-transfer that writes no slot, as go-ethereum's `SetState` still marks it dirty. #[inline] fn transfer_erc20_with_slot( journal: &mut revm::Journal, @@ -760,6 +761,8 @@ fn transfer_erc20_with_slot( where DB: alloy_evm::Database, { + let _ = journal.load_account_mut(token)?; + journal.touch(token); with_journal_checkpoint(journal, |journal| { // Sub amount (checked: reject if insufficient, matching go-ethereum's // changeAltTokenBalanceByState which returns an error on underflow) @@ -796,69 +799,179 @@ where /// Gas limit for internal EVM calls (ERC20 transfer, balanceOf). const EVM_CALL_GAS_LIMIT: u64 = 200_000; -/// Execute an internal EVM call, matching go-ethereum's `evm.Call()` semantics. -/// -/// Unlike `system_call_one_with_caller`, this only runs the handler's `execution()` -/// phase — NOT `execution_result()`. This means: -/// - Logs emitted during the call (e.g., ERC20 Transfer events) remain in the journal -/// - State changes remain in the journal +/// Loads internal-call code without changing the account's access-list temperature. +/// Geth's direct Call/StaticCall resolve code without executing a CALL opcode. /// -/// **Caller is responsible for saving/restoring `evm.tx` if needed.** +/// This is also what puts `address` into `journal.state`, which [`evm_call`] depends on: +/// a `CallValue::Transfer` frame runs `Journal::transfer_loaded`, and its zero-value path +/// is `self.state.get_mut(&to).unwrap()` — a panic, not an error. In an ordinary CALL the +/// account is there because the opcode's `load_acc_and_calc_gas` put it there; an internal +/// call has no opcode, so this is the only load. Resolving the bytecode some other way +/// (caching it, hoisting it, short-circuiting on a known code hash) must keep the load. +fn internal_call_code( + journal: &mut revm::Journal, + address: Address, +) -> Result<(alloy_primitives::B256, revm::state::Bytecode), DB::Error> { + let account = journal.load_account_with_code(address)?; + let was_cold = account.is_cold; + let code = ( + account.info.code_hash(), + account.info.code.clone().unwrap_or_default(), + ); + if was_cold { + journal + .state + .get_mut(&address) + .expect("account was loaded") + .mark_cold(); + } + Ok(code) +} + +/// Executes a fee-token frame while retaining the outer transaction's ORIGIN/GASPRICE. +/// The frame owns its VM checkpoint; a successful call is not rolled back merely +/// because the token's return value or balance delta fails a later business check. fn evm_call( evm: &mut MorphEvm, caller: Address, target: Address, calldata: Bytes, + is_static: bool, ) -> Result> where DB: alloy_evm::Database, { - evm.tx = MorphTxEnv { - inner: revm::context::TxEnv { - caller, - kind: target.into(), - data: calldata, + use revm::context_interface::LocalContextTr; + use revm::interpreter::interpreter_action::FrameInit; + use revm::interpreter::{ + CallInput, CallInputs, CallScheme, CallValue, FrameInput, SharedMemory, + }; + + // Frame execution reports database failures through ctx.error. Check both before + // and after so a refund cannot overwrite an error from the main transaction. + take_context_error(evm)?; + let mut known_bytecode = internal_call_code(evm.ctx_mut().journal_mut(), target)?; + if let Some(delegate) = known_bytecode.1.eip7702_address() { + known_bytecode = internal_call_code(evm.ctx_mut().journal_mut(), delegate)?; + } + // Fee frames are top-level frames that run in the middle of a transaction, so + // neither of revm's truncation points covers them: `free_child_context` only + // releases a *child* frame's region, and `LocalContext::clear` only runs once the + // whole transaction is done. Carve this frame's memory out above whatever the + // shared buffer already holds and release it on the way out, so the main + // transaction frame still starts on zeroed memory the way go-ethereum's + // per-run `NewMemory()` guarantees. The frame keeps using the context's buffer + // rather than one of its own because a nested call hands its callee a + // `CallInput::SharedBuffer` range, and a precompile callee resolves that range + // against the context's buffer (`CallInput::as_bytes`) rather than against the + // calling frame's memory. + let mut fee_frame_memory = + SharedMemory::new_with_buffer(evm.ctx_ref().local().shared_memory_buffer().clone()); + let mut memory = fee_frame_memory.new_child_context(); + memory.set_memory_limit(evm.ctx_ref().cfg().memory_limit()); + let frame = FrameInit { + depth: 0, + memory, + frame_input: FrameInput::Call(Box::new(CallInputs { + input: CallInput::Bytes(calldata), + return_memory_offset: 0..0, gas_limit: EVM_CALL_GAS_LIMIT, - ..Default::default() - }, - ..Default::default() + reservoir: 0, + bytecode_address: target, + known_bytecode, + target_address: target, + caller, + // A zero transfer also performs geth StaticCall's legacy account touch. + value: CallValue::Transfer(U256::ZERO), + scheme: if is_static { + CallScheme::StaticCall + } else { + CallScheme::Call + }, + is_static, + charged_new_account_state_gas: false, + })), }; - let mut h = MorphEvmHandler::::new(); - let init_and_floor_gas = InitialAndFloorGas::new(0, 0); - let mut gas = h.tx_gas(evm, &init_and_floor_gas); - // `execution` owns this checkpoint: it commits once the runtime gas phase is done, or - // unwinds to it when that phase runs out of gas. The `None` arm is only reachable - // under EIP-2780 (AMSTERDAM), which Morph never enables, so it is unreachable today; - // it is kept faithful to upstream so a future hardfork mapping cannot silently skip it. - let checkpoint = evm.ctx().journal_mut().checkpoint(); - match h.execution(evm, checkpoint, &mut gas)? { - Some(res) => Ok(res), - None => h.runtime_oog_result(evm, &init_and_floor_gas, &mut gas), + let result = MorphEvmHandler::::new().run_exec_loop(evm, frame); + fee_frame_memory.free_child_context(); + let result = result?; + take_context_error(evm)?; + Ok(result) +} + +/// Moves a database failure recorded on the context into the return path. +#[inline] +fn take_context_error( + evm: &mut MorphEvm, +) -> Result<(), EVMError> +where + DB: alloy_evm::Database, +{ + revm::context_interface::context::take_error::< + EVMError, + DB::Error, + >(&mut evm.ctx_mut().error) +} + +/// Queries the token using a genuine static frame, as geth's StaticCall does. +/// Successful reads retain access-list warming; writes and malformed results fail. +pub(crate) fn evm_call_balance_of( + evm: &mut MorphEvm, + token: Address, + account: Address, +) -> Result> +where + DB: alloy_evm::Database, +{ + let result = evm_call( + evm, + account, + token, + encode_balance_of_calldata(account), + true, + )?; + let output = &result.interpreter_result().output; + if !result.instruction_result().is_ok() || output.len() < 32 { + return Err(MorphInvalidTransaction::TokenBalanceQueryFailed.into()); } + Ok(U256::from_be_slice(&output[..32])) } -/// Query ERC20 `balanceOf(address)` via an internal EVM call. +/// Resolves the caller's fee-token balance against the **executing** EVM. /// -/// Uses [`with_evm_snapshot`] to match go-ethereum's StaticCall semantics: -/// all state changes and `evm.tx` mutations are reverted after the call. -fn evm_call_balance_of(evm: &mut MorphEvm, token: Address, account: Address) -> U256 +/// go-ethereum reads it through `st.evm` (`GetAltTokenBalanceHybrid`, core/token_gas.go:43), +/// so the `balanceOf` call sees the real block context, the real chain config and the user as +/// `msg.sender`. Building a throwaway EVM here instead — as this path used to, through +/// `system_call_one` — answers under `BlockEnv::default()` and `CfgEnv::default()`: block 0, +/// timestamp 1, chain id 1, zero coinbase and base fee, with `SYSTEM_ADDRESS` as the sender. +/// For any token whose `balanceOf` reads that context the two clients would charge different +/// fees for the same transaction. The gas budget was never the problem: this crate's +/// `system_call_one` set the limit to its own `SYSTEM_CALL_GAS_LIMIT` — `exec.rs`, 200_000, +/// which deliberately shadows revm's `SYSTEM_CALL_GAS_LIMIT` of 30_000_000 at the +/// `SystemCallEvm` impl — and that 200k is go-ethereum's `maxGas`. [`EVM_CALL_GAS_LIMIT`] +/// carries the same number forward. +fn load_token_fee_info( + evm: &mut MorphEvm, + entry: TokenRegistryEntry, + caller: Address, +) -> Result> where DB: alloy_evm::Database, { - with_evm_snapshot(evm, |evm| { - let calldata = encode_balance_of_calldata(account); - match evm_call(evm, Address::ZERO, token, calldata) { - Ok(ref result) if result.instruction_result().is_ok() => { - let output = &result.interpreter_result().output; - if output.len() >= 32 { - U256::from_be_slice(&output[..32]) - } else { - U256::ZERO - } - } - _ => U256::ZERO, - } - }) + let balance = match entry.balance_slot() { + // Slot mode is a plain storage read with no environment to get wrong. It goes + // through the database rather than the journal deliberately: the journal is empty + // at this point in the transaction, and an `sload` here would warm a slot that the + // fee deduction below is careful to leave cold. + Some(slot) => read_balance_from_storage( + evm.ctx_mut().journal_mut().db_mut(), + entry.token_address(), + caller, + slot, + )?, + None => evm_call_balance_of(evm, entry.token_address(), caller)?, + }; + Ok(entry.into_fee_info(caller, balance)) } /// Matches go-ethereum's `transferAltTokenByEVM` validation: @@ -872,6 +985,8 @@ where /// /// `from_balance_before` is the sender's balance before the transfer. If `None`, /// the balance is queried via EVM call (matching go-eth's nil `userBalanceBefore`). +/// Returns the signed refund counter for successful transfers; the deduction phase +/// carries it into transaction gas accounting, while reimbursement ignores it. fn transfer_erc20_with_evm( evm: &mut MorphEvm, from: Address, @@ -879,70 +994,73 @@ fn transfer_erc20_with_evm( token_address: Address, token_amount: U256, from_balance_before: Option, -) -> Result<(), EVMError> +) -> Result> where DB: alloy_evm::Database, { + if token_amount.is_zero() { + return Ok(0); + } // Read sender balance before transfer if not provided. - // This uses with_evm_snapshot internally, so evm.tx is safe. let from_balance_before = match from_balance_before { Some(b) => b, - None => evm_call_balance_of(evm, token_address, from), + None => evm_call_balance_of(evm, token_address, from)?, }; - with_evm_checkpoint(evm, |evm| { - let calldata = build_transfer_calldata(to, token_amount); - let frame_result = evm_call(evm, from, token_address, calldata).map_err(|e| { - EVMError::Transaction(MorphInvalidTransaction::TokenTransferFailed { + // Geth checks affordability before executing the token contract. + let expected_balance = from_balance_before + .checked_sub(token_amount) + .ok_or_else(|| MorphInvalidTransaction::TokenTransferFailed { + reason: format!( + "sender balance {from_balance_before} less than token amount {token_amount}" + ), + })?; + + let calldata = build_transfer_calldata(to, token_amount); + let frame_result = + evm_call(evm, from, token_address, calldata, false).map_err(|e| match e { + EVMError::Database(_) => e, + _ => EVMError::Transaction(MorphInvalidTransaction::TokenTransferFailed { reason: format!("Error: {e:?}"), - }) + }), })?; - if !frame_result.instruction_result().is_ok() { - return Err(MorphInvalidTransaction::TokenTransferFailed { - reason: format!("{:?}", frame_result.interpreter_result()), - } - .into()); + if !frame_result.instruction_result().is_ok() { + return Err(MorphInvalidTransaction::TokenTransferFailed { + reason: format!("{:?}", frame_result.interpreter_result()), } + .into()); + } - // Validate ABI bool return value, matching go-ethereum behavior: - // - No return data: accepted (old tokens that don't return bool) - // - 32+ bytes with last byte == 1: accepted (standard ERC20) - // - Otherwise: rejected - let output = &frame_result.interpreter_result().output; - if !output.is_empty() && (output.len() < 32 || output[31] != 1) { - return Err(MorphInvalidTransaction::TokenTransferFailed { - reason: "alt token transfer returned failure".to_string(), - } - .into()); + // Validate ABI bool return value, matching go-ethereum behavior: + // - No return data: accepted (old tokens that don't return bool) + // - 32+ bytes with last byte == 1: accepted (standard ERC20) + // - Otherwise: rejected + let output = &frame_result.interpreter_result().output; + if !output.is_empty() && (output.len() < 32 || output[31] != 1) { + return Err(MorphInvalidTransaction::TokenTransferFailed { + reason: "alt token transfer returned failure".to_string(), } + .into()); + } - // Verify sender balance changed by the expected amount, matching go-ethereum. - // evm_call_balance_of uses with_evm_snapshot, so evm.tx is safe here too. - let from_balance_after = evm_call_balance_of(evm, token_address, from); - - // Verify sender balance decreased by exactly the transfer amount. - // Matches go-ethereum's transferAltTokenByEVM which always checks this, - // even for self-transfers (from == to), where it would fail because the - // net balance change is zero but the expected decrease is `token_amount`. - let expected_balance = from_balance_before.checked_sub(token_amount).ok_or( - MorphInvalidTransaction::TokenTransferFailed { - reason: format!( - "sender balance {from_balance_before} less than token amount {token_amount}" - ), - }, - )?; - if from_balance_after != expected_balance { - return Err(MorphInvalidTransaction::TokenTransferFailed { - reason: format!( - "sender balance mismatch: expected {expected_balance}, got {from_balance_after}" - ), - } - .into()); + // Verify sender balance changed by the expected amount, matching go-ethereum. + let from_balance_after = evm_call_balance_of(evm, token_address, from)?; + + // Verify sender balance decreased by exactly the transfer amount. + // Matches go-ethereum's transferAltTokenByEVM which always checks this, + // even for self-transfers (from == to), where it would fail because the + // net balance change is zero but the expected decrease is `token_amount`. + if from_balance_after != expected_balance { + return Err(MorphInvalidTransaction::TokenTransferFailed { + reason: format!( + "sender balance mismatch: expected {expected_balance}, got {from_balance_after}" + ), } + .into()); + } - Ok(()) - }) + Ok(frame_result.gas().refunded()) } /// Build the calldata for ERC20 `transfer(address,uint256)` call. @@ -1032,6 +1150,8 @@ fn calculate_caller_fee_with_l1_cost( #[cfg(test)] mod tests { use super::*; + use crate::MorphTxEnv; + use crate::token_fee::tests::{TokenReadFailure, UnreadableTokenDb}; use crate::{ MorphBlockEnv, token_fee::{L2_TOKEN_REGISTRY_ADDRESS, compute_mapping_slot}, @@ -1051,6 +1171,197 @@ mod tests { atomic::{AtomicBool, Ordering}, }; + fn finish_transaction_with_refund( + code: Bytes, + ) -> Result, EVMError> + { + let caller = address!("1000000000000000000000000000000000000001"); + let beneficiary = address!("2000000000000000000000000000000000000002"); + let token = address!("3000000000000000000000000000000000000003"); + let target = address!("4000000000000000000000000000000000000004"); + let mut inner = CacheDB::new(EmptyDB::default()); + inner.insert_account_info( + token, + AccountInfo { + code_hash: keccak256(code.as_ref()), + code: Some(Bytecode::new_raw(code)), + ..Default::default() + }, + ); + let mut evm = MorphEvm::new( + MorphContext::new(UnreadableTokenDb { inner, token }, MorphHardfork::Emerald), + NoOpInspector, + ); + evm.block.inner.beneficiary = beneficiary; + // Produce a real successful main frame. The remainder of this probe enters the + // normal reimbursement and result-finalization phases with an unused gas budget. + let frame = evm_call(&mut evm, caller, target, Bytes::new(), false).unwrap(); + evm.tx = MorphTxEnv { + inner: TxEnv { + tx_type: MORPH_TX_TYPE_ID, + caller, + gas_price: 1, + gas_limit: 30_000, + kind: TxKind::Call(target), + ..Default::default() + }, + fee_token_id: Some(1), + ..Default::default() + }; + evm.cached_token_fee_info = Some(TokenFeeInfo { + token_address: token, + is_active: true, + price_ratio: U256::from(1), + scale: U256::from(1), + caller, + balance: U256::from(100_000), + balance_slot: None, + ..Default::default() + }); + let mut handler = MorphEvmHandler::<_, NoOpInspector>::default(); + handler + .reimburse_caller_token_fee(&mut evm, &Gas::new(1_000)) + .and_then(|_| handler.execution_result(&mut evm, frame, ResultGas::default())) + } + + /// A readable token under [`UnreadableTokenDb`], so any failure reported below comes + /// from the context, not from a read. + fn readable_token_evm() -> MorphEvm { + let token = address!("3000000000000000000000000000000000000003"); + let mut inner = CacheDB::new(EmptyDB::default()); + insert_contract( + &mut inner, + token, + alloy_primitives::bytes!("6000545f5260205ff3"), + ); + inner + .insert_account_storage(token, U256::ZERO, U256::from(42)) + .unwrap(); + MorphEvm::new( + MorphContext::new( + UnreadableTokenDb { + inner, + // Nothing reads this address, so every storage read succeeds. + token: address!("9000000000000000000000000000000000000009"), + }, + MorphHardfork::Emerald, + ), + NoOpInspector, + ) + } + + #[test] + fn a_nested_call_does_not_run_in_a_context_the_main_frame_already_poisoned() { + let token = address!("3000000000000000000000000000000000000003"); + let account = address!("1000000000000000000000000000000000000001"); + let mut evm = readable_token_evm(); + assert_eq!( + evm_call_balance_of(&mut evm, token, account).unwrap(), + U256::from(42), + "sanity: the token is readable" + ); + + // The main frame halted on a failed read; post-execution then reaches this call. + evm.ctx_mut().error = Err(revm::context_interface::context::ContextError::Db( + TokenReadFailure, + )); + let result = evm_call_balance_of(&mut evm, token, account); + assert!( + matches!(result, Err(EVMError::Database(TokenReadFailure))), + "the main frame's failure must be reported, not overwritten by a nested call: {result:?}" + ); + assert!( + evm.ctx_ref().error.is_ok(), + "the failure has been moved into the return path" + ); + } + + #[test] + fn a_refund_after_a_poisoned_main_frame_reports_the_main_frame_failure() { + let caller = address!("1000000000000000000000000000000000000001"); + let token = address!("3000000000000000000000000000000000000003"); + let mut evm = readable_token_evm(); + evm.block.inner.beneficiary = address!("2000000000000000000000000000000000000002"); + evm.tx = MorphTxEnv { + inner: TxEnv { + tx_type: MORPH_TX_TYPE_ID, + caller, + gas_price: 1, + gas_limit: 30_000, + ..Default::default() + }, + fee_token_id: Some(1), + ..Default::default() + }; + evm.cached_token_fee_info = Some(TokenFeeInfo { + token_address: token, + is_active: true, + price_ratio: U256::from(1), + scale: U256::from(1), + caller, + balance: U256::from(100_000), + balance_slot: None, + ..Default::default() + }); + evm.ctx_mut().error = Err(revm::context_interface::context::ContextError::Db( + TokenReadFailure, + )); + + let result = MorphEvmHandler::<_, NoOpInspector>::default() + .reimburse_caller_token_fee(&mut evm, &Gas::new(1_000)); + assert!( + matches!(result, Err(EVMError::Database(TokenReadFailure))), + "{result:?}" + ); + } + + #[test] + fn refund_database_failure_aborts_final_execution_result() { + let result = finish_transaction_with_refund(alloy_primitives::bytes!("6000545f5260205ff3")); + assert!( + matches!(result, Err(EVMError::Database(TokenReadFailure))), + "refund I/O must abort execution, not finalize success without a refund: {result:?}" + ); + } + + #[test] + fn refund_contract_revert_still_allows_transaction_to_finish() { + let result = finish_transaction_with_refund(alloy_primitives::bytes!("5f5ffd")); + assert!( + matches!(result, Ok(ExecutionResult::Success { .. })), + "{result:?}" + ); + } + + #[test] + fn token_transfer_database_failure_is_not_transaction_invalidity() { + let token = address!("3000000000000000000000000000000000000003"); + let from = address!("1000000000000000000000000000000000000001"); + let to = address!("2000000000000000000000000000000000000002"); + let mut inner = CacheDB::new(EmptyDB::default()); + insert_contract( + &mut inner, + token, + alloy_primitives::bytes!("6000545f5260205ff3"), + ); + let mut evm = MorphEvm::new( + MorphContext::new(UnreadableTokenDb { inner, token }, MorphHardfork::Emerald), + NoOpInspector, + ); + let result = transfer_erc20_with_evm( + &mut evm, + from, + to, + token, + U256::from(1), + Some(U256::from(10)), + ); + assert!( + matches!(result, Err(EVMError::Database(TokenReadFailure))), + "{result:?}" + ); + } + fn mutating_return_code(write_value: u8, return_value: u8) -> Bytes { Bytes::from(vec![ 0x60, @@ -1104,6 +1415,77 @@ mod tests { } } + /// ` PUSH0 MSTORE PUSH1 0x20 PUSH0 RETURN` — a `balanceOf` that reports one piece + /// of its environment instead of a balance, so a call made under the wrong environment + /// shows up in the value that comes back. + fn code_returning(opcode: u8) -> Bytes { + Bytes::from(vec![opcode, 0x5f, 0x52, 0x60, 0x20, 0x5f, 0xf3]) + } + + fn insert_contract(db: &mut CacheDB, address: Address, code: Bytes) { + db.insert_account_info( + address, + AccountInfo { + code_hash: keccak256(code.as_ref()), + code: Some(Bytecode::new_raw(code)), + ..Default::default() + }, + ); + } + + /// Loads token 1's registry entry and resolves `caller`'s balance against `evm`. + fn probe_fee_token_balance(db: CacheDB, block: BlockEnv, caller: Address) -> U256 { + let mut evm = MorphEvm::new(MorphContext::new(db, MorphHardfork::Emerald), NoOpInspector); + evm.block = MorphBlockEnv { inner: block }; + + let entry = TokenRegistryEntry::load(evm.ctx_mut().journal_mut().db_mut(), 1) + .unwrap() + .unwrap(); + load_token_fee_info(&mut evm, entry, caller) + .unwrap() + .balance + } + + #[test] + fn fee_token_balance_is_read_under_the_executing_block_environment() { + const TIMESTAMP: u64 = 1_767_765_600; + let token = address!("5300000000000000000000000000000000000042"); + let caller = address!("1000000000000000000000000000000000000001"); + + let mut db = CacheDB::new(EmptyDB::default()); + insert_test_fee_token(&mut db, 1, token, true); + insert_contract(&mut db, token, code_returning(0x42)); // TIMESTAMP + + let balance = probe_fee_token_balance( + db, + BlockEnv { + timestamp: U256::from(TIMESTAMP), + ..Default::default() + }, + caller, + ); + + // `BlockEnv::default()` reports timestamp 1, which is what a throwaway EVM would + // have answered with regardless of the block being executed. + assert_eq!(balance, U256::from(TIMESTAMP)); + } + + #[test] + fn fee_token_balance_query_names_the_queried_account_as_the_caller() { + let token = address!("5300000000000000000000000000000000000042"); + let caller = address!("1000000000000000000000000000000000000001"); + + let mut db = CacheDB::new(EmptyDB::default()); + insert_test_fee_token(&mut db, 1, token, true); + insert_contract(&mut db, token, code_returning(0x33)); // CALLER + + let balance = probe_fee_token_balance(db, BlockEnv::default(), caller); + + // go-ethereum queries as the account being asked about, not as the zero address and + // not as `SYSTEM_ADDRESS`. + assert_eq!(balance, U256::from_be_bytes(caller.into_word().0)); + } + fn insert_test_fee_token( db: &mut CacheDB, token_id: u16, @@ -1251,7 +1633,7 @@ mod tests { } #[test] - fn transfer_erc20_with_evm_reverts_state_on_validation_failure() { + fn transfer_erc20_with_evm_keeps_state_on_post_call_validation_failure() { let from = address!("1000000000000000000000000000000000000001"); let to = address!("2000000000000000000000000000000000000002"); let token = address!("3000000000000000000000000000000000000003"); @@ -1306,7 +1688,7 @@ mod tests { .get(&token) .and_then(|account| account.storage.get(&U256::ZERO)) .unwrap(); - assert_eq!(slot_state.present_value, original_balance); + assert_eq!(slot_state.present_value, U256::from(1)); } #[test] @@ -1352,14 +1734,7 @@ mod tests { err, EVMError::Transaction(MorphInvalidTransaction::TokenTransferFailed { .. }) )); - let slot_state = evm - .ctx_ref() - .journal() - .state - .get(&token) - .and_then(|account| account.storage.get(&U256::ZERO)) - .unwrap(); - assert_eq!(slot_state.present_value, original_balance); + assert!(evm.ctx_ref().journal().state.is_empty()); } #[test] @@ -1386,10 +1761,8 @@ mod tests { inner: BlockEnv::default(), }; + // Nothing loads the token first: the helper has to put it in `journal.state` itself. let journal = evm.ctx_mut().journal_mut(); - let _ = journal.load_account_mut(token).unwrap(); - journal.touch(token); - let err = transfer_erc20_with_slot(journal, from, to, token, U256::from(1), balance_slot) .unwrap_err(); @@ -1432,17 +1805,12 @@ mod tests { inner: BlockEnv::default(), }; - let balance = evm_call_balance_of(&mut evm, token, account); - - assert_eq!(balance, U256::from(42)); - let slot_state = evm - .ctx_ref() - .journal() - .state - .get(&token) - .and_then(|acct| acct.storage.get(&U256::ZERO)) - .unwrap(); - assert_eq!(slot_state.present_value, original_balance); + assert!(evm_call_balance_of(&mut evm, token, account).is_err()); + assert_eq!( + revm::Database::storage(evm.ctx_mut().journal_mut().db_mut(), token, U256::ZERO) + .unwrap(), + original_balance + ); } /// `disable_fee_charge` must leave the caller balance untouched. @@ -1588,4 +1956,432 @@ mod tests { "simulation must not query the fee-token contract or its balance storage" ); } + const FEE_REFUND_TOKEN_ID: u16 = 1; + const FEE_REFUND_GAS_LIMIT: u64 = 100_000; + const FEE_REFUND_GAS_PRICE: u128 = 10; + /// `gas_limit * effective_gas_price` (the L1 data fee is zero with an empty + /// gas-price oracle), converted at scale 1 / price_ratio 1. + const FEE_REFUND_TOKEN_FEE: u64 = 1_000_000; + const FEE_REFUND_CALLER: Address = address!("1000000000000000000000000000000000000001"); + const FEE_REFUND_TOKEN: Address = address!("3000000000000000000000000000000000000003"); + const FEE_REFUND_BENEFICIARY: Address = address!("530000000000000000000000000000000000000a"); + /// Plain EOA target: the main frame does nothing beyond intrinsic gas. + const FEE_REFUND_TARGET: Address = address!("4200000000000000000000000000000000000042"); + + /// Minimal call-mode ERC20: `balance[addr]` lives at slot `uint256(addr)` (no + /// keccak), so `CALLER`, `calldataload(4)` and `balanceOf`'s argument all name the + /// same slot. Dispatch is on `CALLDATASIZE`: + /// - 68 bytes => `transfer(address,uint256)`: SSTORE(caller, SLOAD(caller) - amount), + /// SSTORE(to, SLOAD(to) + amount), return `true`. + /// - anything else => `balanceOf(address)`: return SLOAD(calldataload(4)). + fn fee_refund_slotless_erc20_code() -> Bytes { + Bytes::from(vec![ + 0x36, // CALLDATASIZE + 0x60, 0x44, // PUSH1 68 + 0x14, // EQ + 0x60, 0x13, // PUSH1 19 (transfer JUMPDEST) + 0x57, // JUMPI + // balanceOf(address) + 0x60, 0x04, // PUSH1 4 + 0x35, // CALLDATALOAD + 0x54, // SLOAD + 0x60, 0x00, // PUSH1 0 + 0x52, // MSTORE + 0x60, 0x20, // PUSH1 32 + 0x60, 0x00, // PUSH1 0 + 0xf3, // RETURN + // transfer(address,uint256) + 0x5b, // JUMPDEST (pc 19) + 0x60, 0x24, // PUSH1 36 + 0x35, // CALLDATALOAD -> amount + 0x80, // DUP1 -> amount amount + 0x33, // CALLER -> caller amount amount + 0x54, // SLOAD -> bal_from amount amount + 0x03, // SUB -> bal_from-amount amount + 0x33, // CALLER -> caller new_from amount + 0x55, // SSTORE -> amount + 0x60, 0x04, // PUSH1 4 + 0x35, // CALLDATALOAD -> to amount + 0x80, // DUP1 -> to to amount + 0x54, // SLOAD -> bal_to to amount + 0x82, // DUP3 -> amount bal_to to amount + 0x01, // ADD -> new_to to amount + 0x90, // SWAP1 -> to new_to amount + 0x55, // SSTORE -> amount + 0x50, // POP + 0x60, 0x01, // PUSH1 1 + 0x60, 0x00, // PUSH1 0 + 0x52, // MSTORE + 0x60, 0x20, // PUSH1 32 + 0x60, 0x00, // PUSH1 0 + 0xf3, // RETURN + ]) + } + + fn fee_refund_balance_slot(account: Address) -> U256 { + U256::from_be_bytes(account.into_word().0) + } + + fn fee_refund_evm(payer_token_balance: U256) -> MorphEvm, NoOpInspector> { + let code = fee_refund_slotless_erc20_code(); + let mut db = CacheDB::new(EmptyDB::default()); + db.insert_account_info(FEE_REFUND_CALLER, AccountInfo::default()); + db.insert_account_info( + FEE_REFUND_TOKEN, + AccountInfo { + code_hash: keccak256(code.as_ref()), + code: Some(Bytecode::new_raw(code)), + ..Default::default() + }, + ); + db.insert_account_storage( + FEE_REFUND_TOKEN, + fee_refund_balance_slot(FEE_REFUND_CALLER), + payer_token_balance, + ) + .unwrap(); + // `balanceSlot` word left at zero => call mode (`balance_slot == None`). + insert_test_fee_token(&mut db, FEE_REFUND_TOKEN_ID, FEE_REFUND_TOKEN, true); + + let mut evm = MorphEvm::new( + MorphContext::new(db, MorphHardfork::default()), + NoOpInspector, + ); + evm.block = MorphBlockEnv { + inner: BlockEnv { + basefee: 1, + beneficiary: FEE_REFUND_BENEFICIARY, + gas_limit: 30_000_000, + ..Default::default() + }, + }; + // Production Morph configuration disables the Ethereum calldata gas floor. + evm.cfg.disable_eip7623 = true; + evm + } + + fn fee_refund_present_value( + evm: &MorphEvm, NoOpInspector>, + account: Address, + ) -> U256 { + evm.ctx_ref() + .journal() + .state + .get(&FEE_REFUND_TOKEN) + .and_then(|acct| acct.storage.get(&fee_refund_balance_slot(account))) + .map(|slot| slot.present_value) + .expect("fee-token slot must be in the journal") + } + + /// Runs one call-mode token-fee MorphTx (plain call to an EOA) and returns + /// `(gas_used, final refund applied to the main frame, payer token balance after + /// reimbursement)`. + fn fee_refund_run_token_fee_tx(payer_token_balance: U256) -> (u64, u64, U256) { + let mut evm = fee_refund_evm(payer_token_balance); + let tx = MorphTxEnv { + inner: TxEnv { + tx_type: MORPH_TX_TYPE_ID, + caller: FEE_REFUND_CALLER, + gas_limit: FEE_REFUND_GAS_LIMIT, + gas_price: FEE_REFUND_GAS_PRICE, + kind: TxKind::Call(FEE_REFUND_TARGET), + ..Default::default() + }, + fee_token_id: Some(FEE_REFUND_TOKEN_ID), + ..Default::default() + }; + + let result = evm + .transact_one(tx) + .expect("token-fee MorphTx must execute"); + assert!(result.is_success(), "expected success, got {result:?}"); + let gas_used = result.tx_gas_used(); + let gas_refunded = result.gas().final_refunded(); + + // Sanity: the fee was charged in call mode and equals exactly FEE_REFUND_TOKEN_FEE. + let info = evm + .cached_token_fee_info() + .expect("token fee info is cached"); + assert_eq!( + info.balance_slot, None, + "token must be registered in call mode" + ); + assert_eq!( + info.balance, payer_token_balance, + "balanceOf must see the seeded balance" + ); + assert_eq!( + info.eth_to_token_amount(U256::from( + FEE_REFUND_GAS_LIMIT as u128 * FEE_REFUND_GAS_PRICE + )), + U256::from(FEE_REFUND_TOKEN_FEE) + ); + + ( + gas_used, + gas_refunded, + fee_refund_present_value(&evm, FEE_REFUND_CALLER), + ) + } + + #[test] + fn deduction_sstore_refund_reaches_transaction_gas() { + let fee = U256::from(FEE_REFUND_TOKEN_FEE); + let (gas, refund, balance) = fee_refund_run_token_fee_tx(fee); + assert_eq!((gas, refund), (16_800, 4_200)); + assert_eq!(balance, fee - U256::from(168_000)); + let (gas, refund, balance) = fee_refund_run_token_fee_tx(fee + U256::from(1)); + assert_eq!((gas, refund), (21_000, 0)); + assert_eq!(balance, fee + U256::from(1) - U256::from(210_000)); + } + + #[test] + fn balance_queries_reject_state_writes() { + let mut evm = fee_refund_evm(U256::from(FEE_REFUND_TOKEN_FEE)); + let code = mutating_return_code(1, 1); + evm.ctx_mut().journal_mut().db_mut().insert_account_info( + FEE_REFUND_TOKEN, + AccountInfo { + code_hash: keccak256(&code), + code: Some(Bytecode::new_raw(code)), + ..Default::default() + }, + ); + assert!(evm_call_balance_of(&mut evm, FEE_REFUND_TOKEN, FEE_REFUND_CALLER).is_err()); + } + + #[test] + fn internal_calls_preserve_origin_and_effective_gas_price() { + for (opcode, expected) in [ + (0x32, U256::from_be_slice(FEE_REFUND_CALLER.as_slice())), + (0x3a, U256::from(3)), + ] { + let mut evm = fee_refund_evm(U256::from(FEE_REFUND_TOKEN_FEE)); + evm.tx.inner.caller = FEE_REFUND_CALLER; + evm.tx.inner.tx_type = MORPH_TX_TYPE_ID; + evm.tx.inner.gas_price = 10; + evm.tx.inner.gas_priority_fee = Some(2); + let code = Bytes::from(vec![opcode, 0x5f, 0x52, 0x60, 0x20, 0x5f, 0xf3]); + evm.ctx_mut().journal_mut().db_mut().insert_account_info( + FEE_REFUND_TOKEN, + AccountInfo { + code_hash: keccak256(&code), + code: Some(Bytecode::new_raw(code)), + ..Default::default() + }, + ); + assert_eq!( + evm_call_balance_of(&mut evm, FEE_REFUND_TOKEN, FEE_REFUND_BENEFICIARY).unwrap(), + expected + ); + assert_eq!(evm.tx.inner.caller, FEE_REFUND_CALLER); + } + } + + #[test] + fn zero_token_transfer_does_not_call_the_contract() { + let mut evm = fee_refund_evm(U256::ZERO); + let code = mutating_return_code(1, 0); + evm.ctx_mut().journal_mut().db_mut().insert_account_info( + FEE_REFUND_TOKEN, + AccountInfo { + code_hash: keccak256(&code), + code: Some(Bytecode::new_raw(code)), + ..Default::default() + }, + ); + transfer_erc20_with_evm( + &mut evm, + FEE_REFUND_CALLER, + FEE_REFUND_BENEFICIARY, + FEE_REFUND_TOKEN, + U256::ZERO, + Some(U256::ZERO), + ) + .unwrap(); + assert!(evm.ctx_ref().journal().state.is_empty()); + } + + /// Runs one transaction against `code` deployed at [`FEE_REFUND_TARGET`] and + /// returns what the main frame returned. `fee_token_id` selects the call-mode + /// token-fee path or the ordinary ETH-fee path. + fn fee_refund_run_probe(code: Bytes, fee_token_id: Option) -> Bytes { + let mut evm = fee_refund_evm(U256::from(FEE_REFUND_TOKEN_FEE)); + let db = evm.ctx_mut().journal_mut().db_mut(); + insert_contract(db, FEE_REFUND_TARGET, code); + db.insert_account_info( + FEE_REFUND_CALLER, + AccountInfo { + balance: U256::from(FEE_REFUND_GAS_LIMIT as u128 * FEE_REFUND_GAS_PRICE), + ..Default::default() + }, + ); + let tx = MorphTxEnv { + inner: TxEnv { + tx_type: if fee_token_id.is_some() { + MORPH_TX_TYPE_ID + } else { + 0 + }, + caller: FEE_REFUND_CALLER, + gas_limit: FEE_REFUND_GAS_LIMIT, + gas_price: FEE_REFUND_GAS_PRICE, + kind: TxKind::Call(FEE_REFUND_TARGET), + ..Default::default() + }, + fee_token_id, + ..Default::default() + }; + let result = evm.transact_one(tx).expect("probe must execute"); + assert!(result.is_success(), "expected success, got {result:?}"); + result.output().cloned().unwrap_or_default() + } + + /// Minimal proxy: copies its calldata into memory and `DELEGATECALL`s + /// `implementation`, the way mainnet's call-mode fee tokens reach theirs. + fn delegating_proxy_code(implementation: Address) -> Bytes { + let mut code = vec![ + 0x36, // CALLDATASIZE (size) + 0x5f, // PUSH0 (offset) + 0x5f, // PUSH0 (destOffset) + 0x37, // CALLDATACOPY + 0x5f, // PUSH0 (retSize) + 0x5f, // PUSH0 (retOffset) + 0x36, // CALLDATASIZE (argsSize) + 0x5f, // PUSH0 (argsOffset) + 0x73, // PUSH20 implementation + ]; + code.extend_from_slice(implementation.as_slice()); + code.extend_from_slice(&[ + 0x5a, // GAS + 0xf4, // DELEGATECALL + 0x3d, // RETURNDATASIZE (size) + 0x5f, // PUSH0 (offset) + 0x5f, // PUSH0 (destOffset) + 0x3e, // RETURNDATACOPY + 0x50, // POP (DELEGATECALL success flag) + 0x3d, // RETURNDATASIZE (size) + 0x5f, // PUSH0 (offset) + 0xf3, // RETURN + ]); + Bytes::from(code) + } + + /// Same ERC20, except `balanceOf` returns its result through the identity + /// precompile, reading the precompile's arguments out of memory. + fn fee_refund_precompile_erc20_code() -> Bytes { + let mut code = vec![ + 0x36, // CALLDATASIZE + 0x60, 0x44, // PUSH1 68 + 0x14, // EQ + 0x60, 0x1e, // PUSH1 30 (transfer JUMPDEST) + 0x57, // JUMPI + // balanceOf(address), answered by identity(mem[0..32]) + 0x60, 0x04, // PUSH1 4 + 0x35, // CALLDATALOAD + 0x54, // SLOAD + 0x5f, // PUSH0 + 0x52, // MSTORE + 0x60, 0x20, // PUSH1 32 (retSize) + 0x60, 0x20, // PUSH1 32 (retOffset) + 0x60, 0x20, // PUSH1 32 (argsSize) + 0x5f, // PUSH0 (argsOffset) + 0x60, 0x04, // PUSH1 4 (identity precompile) + 0x5a, // GAS + 0xfa, // STATICCALL + 0x50, // POP + 0x60, 0x20, // PUSH1 32 (size) + 0x60, 0x20, // PUSH1 32 (offset) + 0xf3, // RETURN + ]; + assert_eq!(code.len(), 30, "the transfer JUMPDEST moved"); + code.extend_from_slice(&fee_refund_slotless_erc20_code()[19..]); + Bytes::from(code) + } + + /// A precompile called from inside a fee frame resolves its arguments against + /// the context's shared buffer, so the fee frames have to keep writing into that + /// buffer rather than into one of their own. + #[test] + fn fee_token_frames_reach_a_precompile_through_memory() { + let fee = U256::from(FEE_REFUND_TOKEN_FEE); + let mut evm = fee_refund_evm(fee); + let db = evm.ctx_mut().journal_mut().db_mut(); + insert_contract(db, FEE_REFUND_TOKEN, fee_refund_precompile_erc20_code()); + assert_eq!( + fee_refund_run_fee_tx(&mut evm), + fee_refund_run_token_fee_tx(fee) + ); + } + + /// Mainnet's call-mode fee tokens are proxies, so a fee frame's nested + /// `DELEGATECALL` reads its calldata back out of the frame's memory. + #[test] + fn fee_token_frames_reach_a_delegating_proxy_implementation() { + const IMPLEMENTATION: Address = address!("3000000000000000000000000000000000000004"); + let fee = U256::from(FEE_REFUND_TOKEN_FEE); + let mut evm = fee_refund_evm(fee); + let db = evm.ctx_mut().journal_mut().db_mut(); + insert_contract(db, IMPLEMENTATION, fee_refund_slotless_erc20_code()); + insert_contract(db, FEE_REFUND_TOKEN, delegating_proxy_code(IMPLEMENTATION)); + + // Indistinguishable from the same transaction against an unproxied token: + // the implementation saw exactly the calldata the fee frame wrote. + assert_eq!( + fee_refund_run_fee_tx(&mut evm), + fee_refund_run_token_fee_tx(fee) + ); + } + + /// Runs the standard call-mode token-fee MorphTx on an already-built `evm` and + /// reports it the way [`fee_refund_run_token_fee_tx`] does. + fn fee_refund_run_fee_tx( + evm: &mut MorphEvm, NoOpInspector>, + ) -> (u64, u64, U256) { + let result = evm + .transact_one(MorphTxEnv { + inner: TxEnv { + tx_type: MORPH_TX_TYPE_ID, + caller: FEE_REFUND_CALLER, + gas_limit: FEE_REFUND_GAS_LIMIT, + gas_price: FEE_REFUND_GAS_PRICE, + kind: TxKind::Call(FEE_REFUND_TARGET), + ..Default::default() + }, + fee_token_id: Some(FEE_REFUND_TOKEN_ID), + ..Default::default() + }) + .expect("token-fee MorphTx must execute"); + assert!(result.is_success(), "expected success, got {result:?}"); + ( + result.tx_gas_used(), + result.gas().final_refunded(), + fee_refund_present_value(evm, FEE_REFUND_CALLER), + ) + } + + /// go-ethereum allocates a fresh `Memory` for every interpreter run + /// (`core/vm/interpreter.go`), so a transaction's frame always starts on zeroed + /// memory. The fee-token frames run on the transaction's shared memory buffer, + /// so they have to hand it back the length they found it at. + #[test] + fn fee_token_frames_do_not_leak_memory_into_the_main_frame() { + // `MSIZE` and `MLOAD(0)`, each returned as the frame's 32-byte output. + for probe in [ + vec![0x59, 0x5f, 0x52, 0x60, 0x20, 0x5f, 0xf3], + vec![0x5f, 0x51, 0x5f, 0x52, 0x60, 0x20, 0x5f, 0xf3], + ] { + let code = Bytes::from(probe); + assert_eq!( + U256::from_be_slice(&fee_refund_run_probe(code.clone(), None)), + U256::ZERO, + "ETH-fee control" + ); + assert_eq!( + U256::from_be_slice(&fee_refund_run_probe(code, Some(FEE_REFUND_TOKEN_ID))), + U256::ZERO, + "a token-fee main frame must start on zeroed memory too" + ); + } + } } diff --git a/crates/revm/src/lib.rs b/crates/revm/src/lib.rs index 00aea498..a887c914 100644 --- a/crates/revm/src/lib.rs +++ b/crates/revm/src/lib.rs @@ -73,7 +73,7 @@ pub use l1block::{ }; pub use precompiles::MorphPrecompiles; pub use token_fee::{ - L2_TOKEN_REGISTRY_ADDRESS, TokenFeeInfo, compute_mapping_slot, - compute_mapping_slot_for_address, encode_balance_of_calldata, query_erc20_balance, + L2_TOKEN_REGISTRY_ADDRESS, MorphEvmEnv, TokenFeeInfo, compute_mapping_slot, + compute_mapping_slot_for_address, encode_balance_of_calldata, }; pub use tx::{MorphTxEnv, MorphTxExt}; diff --git a/crates/revm/src/token_fee.rs b/crates/revm/src/token_fee.rs index c32827cb..7b1b6574 100644 --- a/crates/revm/src/token_fee.rs +++ b/crates/revm/src/token_fee.rs @@ -9,12 +9,18 @@ use alloy_evm::Database; use alloy_primitives::{Address, Bytes, U256, address, keccak256}; use morph_chainspec::hardfork::MorphHardfork; use revm::Database as RevmDatabase; -use revm::SystemCallEvm; use revm::{context_interface::result::EVMError, inspector::NoOpInspector}; -use crate::evm::MorphContext; use crate::{MorphEvm, MorphInvalidTransaction}; +/// The environment a fee-token `balanceOf` call is evaluated in. +/// +/// Produced by `ConfigureEvm::evm_env` for the block whose state is being read, so the pool +/// resolves the same balance the execution layer would. go-ethereum builds the equivalent +/// `vm.BlockContext` from the header before querying a call-mode token +/// (`pool.getBalanceFunc`, core/tx_pool.go:330). +pub type MorphEvmEnv = alloy_evm::EvmEnv; + /// L2 Token Registry contract address on Morph L2. /// Reference: pub const L2_TOKEN_REGISTRY_ADDRESS: Address = address!("5300000000000000000000000000000000000021"); @@ -60,6 +66,18 @@ pub(crate) struct TokenRegistryEntry { } impl TokenRegistryEntry { + /// The registered ERC20 contract. + pub(crate) const fn token_address(&self) -> Address { + self.token_address + } + + /// The caller's balance storage slot, when the registry declares one. + /// + /// `None` means call mode: the balance has to be read by calling `balanceOf`. + pub(crate) const fn balance_slot(&self) -> Option { + self.balance_slot + } + /// Load fee-token metadata without reading a caller's token balance. pub(crate) fn load( db: &mut DB, @@ -84,14 +102,14 @@ impl TokenRegistryEntry { self, db: &mut DB, caller: Address, - hardfork: MorphHardfork, - ) -> Result { + env: &MorphEvmEnv, + ) -> Result> { let balance = read_token_balance_with_fallback( db, self.token_address, caller, self.balance_slot, - hardfork, + env, )?; Ok(self.into_fee_info(caller, balance)) } @@ -109,7 +127,7 @@ impl TokenRegistryEntry { Ok(self.into_fee_info(caller, balance)) } - fn into_fee_info(self, caller: Address, balance: U256) -> TokenFeeInfo { + pub(crate) fn into_fee_info(self, caller: Address, balance: U256) -> TokenFeeInfo { TokenFeeInfo { token_address: self.token_address, is_active: self.is_active, @@ -124,6 +142,16 @@ impl TokenRegistryEntry { } impl TokenFeeInfo { + /// Maximum permitted token debit, bounded by the available balance. + /// A zero fee limit means that the whole balance is available. + pub fn effective_fee_limit(&self, fee_limit: U256) -> U256 { + if fee_limit.is_zero() { + self.balance + } else { + self.balance.min(fee_limit) + } + } + /// Load token fee information with EVM call fallback. /// /// Reads token parameters from L2 Token Registry storage. If the token's @@ -133,14 +161,14 @@ impl TokenFeeInfo { db: &mut DB, token_id: u16, caller: Address, - hardfork: MorphHardfork, - ) -> Result, DB::Error> { + env: &MorphEvmEnv, + ) -> Result, EVMError> { let entry = match TokenRegistryEntry::load(db, token_id)? { Some(e) => e, None => return Ok(None), }; - entry.load_for_caller(db, caller, hardfork).map(Some) + entry.load_for_caller(db, caller, env).map(Some) } /// Storage-only variant of [`Self::load_for_caller`]. @@ -292,26 +320,33 @@ fn read_token_balance_with_fallback( token: Address, account: Address, balance_slot: Option, - hardfork: MorphHardfork, -) -> Result { + env: &MorphEvmEnv, +) -> Result> { if let Some(slot) = balance_slot { - return read_balance_from_storage(db, token, account, slot); + return Ok(read_balance_from_storage(db, token, account, slot)?); } - // EVM fallback: construct temporary MorphEvm for balanceOf call + // Call mode: stand the EVM up in the caller's environment rather than a default one, + // and make the same `balanceOf` call the execution layer makes, so both reach the same + // answer for a token whose balance depends on block context or `msg.sender`. let db: &mut dyn Database = db; - let mut evm = MorphEvm::new(MorphContext::new(db, hardfork), NoOpInspector {}); - - match query_balance_via_system_call(&mut evm, token, account) { - Ok(balance) => Ok(balance), - Err(EVMError::Database(e)) => Err(e), - Err(_) => Ok(U256::ZERO), // Non-DB errors → zero (safe fallback) - } + let mut evm = MorphEvm::from_env(db, env.clone(), NoOpInspector {}); + // ORIGIN follows this client's own execution layer, which resolves the same + // `balanceOf` against the transaction's `caller`. go-ethereum's pool instead builds + // its query on an empty `vm.TxContext{}` (core/tx_pool.go:341), leaving ORIGIN at the + // zero address and disagreeing with go-ethereum's own execution layer. Admission + // exists to predict what the builder will be able to include, so it follows execution + // rather than the other client's pool. GASPRICE is the one input this query still + // cannot match: it stays at the `TxEnv` default of zero because the effective price + // depends on the next block's base fee, which admission does not know. go-ethereum's + // pool has the same gap. + evm.tx.inner.caller = account; + crate::handler::evm_call_balance_of(&mut evm, token, account) } /// Read ERC20 balance directly from storage slot. #[inline] -fn read_balance_from_storage( +pub(crate) fn read_balance_from_storage( db: &mut DB, token: Address, account: Address, @@ -322,44 +357,6 @@ fn read_balance_from_storage( read_mapping_value(db, token, balance_slot, &key) } -/// Execute EVM `balanceOf(address)` call. -fn query_balance_via_system_call( - evm: &mut MorphEvm, - token: Address, - account: Address, -) -> Result> -where - DB: Database, -{ - let calldata = encode_balance_of_calldata(account); - match evm.system_call_one(token, calldata) { - Ok(result) if result.is_success() => { - if let Some(output) = result.output() - && output.len() >= 32 - { - return Ok(U256::from_be_slice(&output[..32])); - } - Ok(U256::ZERO) - } - Ok(_) => Ok(U256::ZERO), - Err(_) => Ok(U256::ZERO), - } -} - -/// Query ERC20 balance via EVM call. -/// -/// Use this when you have a `MorphEvm` instance and need to call `balanceOf`. -pub fn query_erc20_balance( - evm: &mut MorphEvm, - token: Address, - account: Address, -) -> Result> -where - DB: Database, -{ - query_balance_via_system_call(evm, token, account) -} - /// Encode ERC20 `balanceOf(address)` calldata. /// /// Function selector: `0x70a08231` @@ -373,9 +370,166 @@ pub fn encode_balance_of_calldata(account: Address) -> Bytes { } #[cfg(test)] -mod tests { +pub(crate) mod tests { use super::*; + use alloy_primitives::{B256, address, bytes}; + use revm::bytecode::Bytecode; + use revm::database::{CacheDB, EmptyDB}; + use revm::state::AccountInfo; + + /// The storage read failure injected by [`UnreadableTokenDb`], distinguishable from any + /// error a real database would report. Shared with the handler tests. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub(crate) struct TokenReadFailure; + + impl core::fmt::Display for TokenReadFailure { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("injected token storage read failure") + } + } + + impl core::error::Error for TokenReadFailure {} + + impl revm::database_interface::DBErrorMarker for TokenReadFailure {} + + /// Fails every storage read of `token` with [`TokenReadFailure`]; everything else reads + /// normally, so a failure a test observes comes from that token's storage. Shared with the + /// handler tests. + #[derive(Debug)] + pub(crate) struct UnreadableTokenDb { + pub(crate) inner: CacheDB, + pub(crate) token: Address, + } + + impl RevmDatabase for UnreadableTokenDb { + type Error = TokenReadFailure; + + fn basic(&mut self, address: Address) -> Result, Self::Error> { + Ok(self.inner.basic(address).unwrap()) + } + + fn code_by_hash(&mut self, code_hash: B256) -> Result { + Ok(self.inner.code_by_hash(code_hash).unwrap()) + } + + fn storage(&mut self, address: Address, index: U256) -> Result { + if address == self.token { + return Err(TokenReadFailure); + } + Ok(self.inner.storage(address, index).unwrap()) + } + + fn block_hash(&mut self, number: u64) -> Result { + Ok(self.inner.block_hash(number).unwrap()) + } + } + + /// Registry state for a call-mode token (no `balanceSlot`) whose `balanceOf` returns + /// storage slot 0, so reading it is a storage read of the token contract. + fn call_mode_token_state(token: Address, balance: u64) -> CacheDB { + call_mode_token_state_with_code(token, balance, bytes!("6000545f5260205ff3")) + } + + /// As [`call_mode_token_state`], with an explicit `balanceOf` implementation. + fn call_mode_token_state_with_code( + token: Address, + balance: u64, + code: Bytes, + ) -> CacheDB { + let mut db = CacheDB::new(EmptyDB::default()); + let mut token_id_bytes = [0u8; 32]; + token_id_bytes[31] = 1; + let base = compute_mapping_slot(TOKEN_REGISTRY_SLOT, &token_id_bytes); + + let mut packed = [0u8; 32]; + packed[30] = 18; // decimals + packed[31] = 1; // isActive + for (slot, value) in [ + (base, U256::from_be_bytes(token.into_word().0)), + // Zero means "no known balance slot": the EVM `balanceOf` fallback is used. + (base + U256::from(1), U256::ZERO), + (base + U256::from(2), U256::from_be_bytes(packed)), + (base + U256::from(3), U256::from(1)), // scale + ( + compute_mapping_slot(PRICE_RATIO_SLOT, &token_id_bytes), + U256::from(1), // priceRatio + ), + ] { + db.insert_account_storage(L2_TOKEN_REGISTRY_ADDRESS, slot, value) + .unwrap(); + } + + db.insert_account_info( + token, + AccountInfo { + code_hash: alloy_primitives::keccak256(code.as_ref()), + code: Some(Bytecode::new_raw(code)), + ..Default::default() + }, + ); + db.insert_account_storage(token, U256::ZERO, U256::from(balance)) + .unwrap(); + db + } + + #[test] + fn call_mode_balance_is_read_under_the_supplied_block_environment() { + const TIMESTAMP: u64 = 1_767_765_600; + let token = address!("5300000000000000000000000000000000000042"); + let caller = address!("0000000000000000000000000000000000000001"); + + // TIMESTAMP PUSH0 MSTORE PUSH1 0x20 PUSH0 RETURN — a `balanceOf` that reports the + // block time, so an answer produced under the wrong environment is visible. + let mut db = call_mode_token_state_with_code(token, 0, bytes!("425f5260205ff3")); + + let env = MorphEvmEnv::new( + revm::context::CfgEnv::new_with_spec(MorphHardfork::Emerald), + crate::MorphBlockEnv { + inner: revm::context::BlockEnv { + timestamp: U256::from(TIMESTAMP), + ..Default::default() + }, + }, + ); + + let info = TokenFeeInfo::load_for_caller(&mut db, 1, caller, &env) + .unwrap() + .unwrap(); + + // `BlockEnv::default()` reports timestamp 1, which is what the pool answered with + // regardless of the block it was validating against. + assert_eq!(info.balance, U256::from(TIMESTAMP)); + } + + #[test] + fn balance_of_fallback_reports_a_failed_state_read_instead_of_a_zero_balance() { + let token = address!("5300000000000000000000000000000000000042"); + let caller = address!("0000000000000000000000000000000000000001"); + let env = MorphEvmEnv::new( + revm::context::CfgEnv::new_with_spec(MorphHardfork::Emerald), + crate::MorphBlockEnv::default(), + ); + + // Readable state: the fallback reaches the token and reads the balance. + let mut readable = call_mode_token_state(token, 10_000_000); + let info = TokenFeeInfo::load_for_caller(&mut readable, 1, caller, &env) + .unwrap() + .unwrap(); + assert_eq!(info.balance, U256::from(10_000_000)); + + // Same state, but the token's storage cannot be read. Reporting a zero balance here + // would be indistinguishable from an account that genuinely cannot pay. + let mut unreadable = UnreadableTokenDb { + inner: call_mode_token_state(token, 10_000_000), + token, + }; + assert_eq!( + TokenFeeInfo::load_for_caller(&mut unreadable, 1, caller, &env).unwrap_err(), + EVMError::Database(TokenReadFailure) + ); + } + #[test] fn test_token_fee_info_default() { let info = TokenFeeInfo::default(); diff --git a/crates/txpool/src/morph_tx_validation.rs b/crates/txpool/src/morph_tx_validation.rs index e666c295..b6abf576 100644 --- a/crates/txpool/src/morph_tx_validation.rs +++ b/crates/txpool/src/morph_tx_validation.rs @@ -8,7 +8,7 @@ use alloy_evm::Database; use alloy_primitives::{Address, U256}; use morph_chainspec::hardfork::MorphHardfork; use morph_primitives::{MorphTxEnvelope, transaction::morph_transaction::MORPH_TX_VERSION_1}; -use morph_revm::TokenFeeInfo; +use morph_revm::{MorphEvmEnv, TokenFeeInfo}; use crate::MorphTxError; @@ -107,7 +107,15 @@ pub fn validate_morph_tx( }); } - let token_info = TokenFeeInfo::load_for_caller(db, fee_token_id, input.sender, input.hardfork) + // Pool admission has no block environment, so a call-mode token's `balanceOf` + // is evaluated under the hardfork's defaults. That matches the pool's previous + // behaviour; threading the real head environment through admission is txpool + // work and does not belong in this change. + let env = MorphEvmEnv::new( + reth_revm::revm::context::CfgEnv::new_with_spec(input.hardfork), + morph_revm::MorphBlockEnv::default(), + ); + let token_info = TokenFeeInfo::load_for_caller(db, fee_token_id, input.sender, &env) .map_err(|err| MorphTxError::TokenInfoFetchFailed { token_id: fee_token_id, message: format!("{err:?}"), @@ -136,14 +144,9 @@ pub fn validate_morph_tx( let total_token_fee = token_gas_fee.saturating_add(input.l1_data_fee); let required_token_amount = token_info.eth_to_token_amount(total_token_fee); - // Match REVM semantics: - // - fee_limit == 0 => use token balance as effective limit - // - fee_limit > balance => cap by token balance - let effective_limit = if fee_limit.is_zero() || fee_limit > token_info.balance { - token_info.balance - } else { - fee_limit - }; + // Share the execution layer's clamp rather than restating it: a zero `fee_limit` + // means the whole token balance, and a larger one is capped by it. + let effective_limit = token_info.effective_fee_limit(fee_limit); // Check token balance against effective limit. if effective_limit < required_token_amount {