@@ -1212,6 +1212,266 @@ fn state_gas_reservoir_does_not_leak_across_frames() {
12121212 ) ;
12131213}
12141214
1215+ const SSTORE_SET_THEN_CLEAR_CODE : & [ u8 ] = & [
1216+ 0x60 , 0x01 , 0x60 , 0x00 , 0x55 , 0x60 , 0x00 , 0x60 , 0x00 , 0x55 , 0x00 ,
1217+ ] ;
1218+ const SSTORE_SET_THEN_CLEAR_THEN_REVERT_CODE : & [ u8 ] = & [
1219+ 0x60 , 0x01 , 0x60 , 0x00 , 0x55 , 0x60 , 0x00 , 0x60 , 0x00 , 0x55 , 0x60 , 0x00 , 0x60 , 0x00 , 0xFD ,
1220+ ] ;
1221+ const SSTORE_RESTORE_EMPTY_REFUND : u64 = 10_000 ;
1222+
1223+ fn cold_sload_burner ( slots : std:: ops:: Range < u8 > ) -> Vec < u8 > {
1224+ let mut code = Vec :: new ( ) ;
1225+ for slot in slots {
1226+ code. extend_from_slice ( & [ 0x60 , slot, 0x54 , 0x50 ] ) ;
1227+ }
1228+ code
1229+ }
1230+
1231+ fn default_frame ( target : Address , flags : u8 , gas_limit : u64 ) -> Frame {
1232+ Frame {
1233+ mode : u8:: from ( FrameMode :: Default ) ,
1234+ flags,
1235+ target : Some ( target) ,
1236+ gas_limit,
1237+ value : U256 :: zero ( ) ,
1238+ data : Bytes :: new ( ) ,
1239+ }
1240+ }
1241+
1242+ fn gross_gas_used ( tx : & FrameTransaction , report : & ExecutionReport ) -> u64 {
1243+ let sum_frame_limits: u64 = tx. frames . iter ( ) . map ( |f| f. gas_limit ) . sum ( ) ;
1244+ let frame_gas: u64 = report
1245+ . frame_results
1246+ . as_ref ( )
1247+ . expect ( "frame tx report must carry per-frame results" )
1248+ . iter ( )
1249+ . map ( |( _, gas, _) | * gas)
1250+ . sum ( ) ;
1251+ tx. total_gas_limit ( ) - sum_frame_limits + frame_gas
1252+ }
1253+
1254+ #[ test]
1255+ fn successful_frame_refund_nets_payer_and_block_gas ( ) {
1256+ let wallet = Address :: from_low_u64_be ( 0x3529 ) ;
1257+ let writer = Address :: from_low_u64_be ( 0x352A ) ;
1258+ let wallet_initial = U256 :: from ( 10u64 ) . pow ( U256 :: from ( 18u64 ) ) ;
1259+ let mut writer_code = cold_sload_burner ( 0x10 ..0x28 ) ;
1260+ writer_code. extend_from_slice ( SSTORE_SET_THEN_CLEAR_CODE ) ;
1261+ let mut tx = frame_tx_with_frames ( vec ! [
1262+ verify_frame( FUNDED_SENDER ) ,
1263+ verify_frame( wallet) ,
1264+ Frame {
1265+ mode: u8 :: from( FrameMode :: Sender ) ,
1266+ flags: 0 ,
1267+ target: Some ( writer) ,
1268+ gas_limit: 300_000 ,
1269+ value: U256 :: zero( ) ,
1270+ data: Bytes :: new( ) ,
1271+ } ,
1272+ ] ) ;
1273+ tx. max_fee_per_gas = 100_000_000_000 ;
1274+ tx. max_priority_fee_per_gas = 2_000_000_000 ;
1275+ let ( result, db) = run_frame_tx_with_fees (
1276+ & [
1277+ (
1278+ FUNDED_SENDER ,
1279+ AUTO_SEED_SENDER_BALANCE ,
1280+ 0 ,
1281+ Bytes :: from ( APPROVE_EXECUTION_CODE . to_vec ( ) ) ,
1282+ ) ,
1283+ (
1284+ wallet,
1285+ wallet_initial,
1286+ 0 ,
1287+ Bytes :: from ( APPROVE_PAYMENT_CODE . to_vec ( ) ) ,
1288+ ) ,
1289+ ( writer, U256 :: zero ( ) , 0 , Bytes :: from ( writer_code) ) ,
1290+ ] ,
1291+ tx. clone ( ) ,
1292+ 10_000_000_000 ,
1293+ ) ;
1294+ let report = result. expect ( "valid frame tx (sender approved, payer set)" ) ;
1295+ assert ! ( matches!( report. result, TxResult :: Success ) ) ;
1296+ let gross = gross_gas_used ( & tx, & report) ;
1297+ assert ! (
1298+ gross / 5 > SSTORE_RESTORE_EMPTY_REFUND ,
1299+ "test setup must keep the refund cap non-binding (gross = {gross})"
1300+ ) ;
1301+ let net = gross - SSTORE_RESTORE_EMPTY_REFUND ;
1302+ assert_eq ! ( report. gas_used, net, "gas_used must be net of the refund" ) ;
1303+ assert_eq ! ( report. gas_spent, net, "gas_spent must be net of the refund" ) ;
1304+ let effective = U256 :: from ( 12_000_000_000u64 ) ;
1305+ let payer_delta = wallet_initial - balance_of ( & db, wallet) ;
1306+ assert_eq ! (
1307+ payer_delta,
1308+ effective * U256 :: from( net) ,
1309+ "payer must be charged for net (post-refund) gas"
1310+ ) ;
1311+ let coinbase_gain = balance_of ( & db, COINBASE_ADDR ) ;
1312+ let base_burn = U256 :: from ( 10_000_000_000u64 ) * U256 :: from ( net) ;
1313+ assert_eq ! (
1314+ payer_delta,
1315+ coinbase_gain + base_burn,
1316+ "coinbase fee must be paid on net (post-refund) gas"
1317+ ) ;
1318+ }
1319+
1320+ #[ test]
1321+ fn reverted_frame_refund_contribution_is_discarded ( ) {
1322+ let writer_ok = Address :: from_low_u64_be ( 0x352B ) ;
1323+ let writer_rev = Address :: from_low_u64_be ( 0x352C ) ;
1324+ let mut ok_code = cold_sload_burner ( 0x10 ..0x28 ) ;
1325+ ok_code. extend_from_slice ( SSTORE_SET_THEN_CLEAR_CODE ) ;
1326+ let tx = frame_tx_with_frames ( vec ! [
1327+ verify_frame( FUNDED_SENDER ) ,
1328+ default_frame( writer_ok, 0x00 , 300_000 ) ,
1329+ default_frame( writer_rev, 0x00 , 300_000 ) ,
1330+ ] ) ;
1331+ let ( result, _db) = run_frame_tx (
1332+ & [
1333+ (
1334+ FUNDED_SENDER ,
1335+ AUTO_SEED_SENDER_BALANCE ,
1336+ 0 ,
1337+ Bytes :: from ( APPROVE_BOTH_CODE . to_vec ( ) ) ,
1338+ ) ,
1339+ ( writer_ok, U256 :: zero ( ) , 0 , Bytes :: from ( ok_code) ) ,
1340+ (
1341+ writer_rev,
1342+ U256 :: zero ( ) ,
1343+ 0 ,
1344+ Bytes :: from ( SSTORE_SET_THEN_CLEAR_THEN_REVERT_CODE . to_vec ( ) ) ,
1345+ ) ,
1346+ ] ,
1347+ tx. clone ( ) ,
1348+ ) ;
1349+ let report = result. expect ( "valid frame tx (reverted DEFAULT frame, payer approved)" ) ;
1350+ let fr = report
1351+ . frame_results
1352+ . as_ref ( )
1353+ . expect ( "frame results present" ) ;
1354+ assert_eq ! (
1355+ fr[ 2 ] . 0 ,
1356+ ethrex_common:: types:: FRAME_RECEIPT_STATUS_FAILURE ,
1357+ "the reverting frame must be reported as failed"
1358+ ) ;
1359+ let gross = gross_gas_used ( & tx, & report) ;
1360+ assert ! (
1361+ gross / 5 > SSTORE_RESTORE_EMPTY_REFUND ,
1362+ "test setup must keep the refund cap non-binding (gross = {gross})"
1363+ ) ;
1364+ assert_eq ! (
1365+ report. gas_used,
1366+ gross - SSTORE_RESTORE_EMPTY_REFUND ,
1367+ "only the successful frame's refund may be applied"
1368+ ) ;
1369+ }
1370+
1371+ #[ test]
1372+ fn reverted_atomic_batch_refund_contributions_are_discarded ( ) {
1373+ let writer_ok = Address :: from_low_u64_be ( 0x352D ) ;
1374+ let writer_batched = Address :: from_low_u64_be ( 0x352E ) ;
1375+ let reverter = Address :: from_low_u64_be ( 0x352F ) ;
1376+ let mut ok_code = cold_sload_burner ( 0x10 ..0x28 ) ;
1377+ ok_code. extend_from_slice ( SSTORE_SET_THEN_CLEAR_CODE ) ;
1378+ let tx = frame_tx_with_frames ( vec ! [
1379+ verify_frame( FUNDED_SENDER ) ,
1380+ default_frame( writer_ok, 0x00 , 300_000 ) ,
1381+ default_frame( writer_batched, 0x04 , 200_000 ) ,
1382+ default_frame( reverter, 0x00 , 30_000 ) ,
1383+ ] ) ;
1384+ let ( result, _db) = run_frame_tx (
1385+ & [
1386+ (
1387+ FUNDED_SENDER ,
1388+ AUTO_SEED_SENDER_BALANCE ,
1389+ 0 ,
1390+ Bytes :: from ( APPROVE_BOTH_CODE . to_vec ( ) ) ,
1391+ ) ,
1392+ ( writer_ok, U256 :: zero ( ) , 0 , Bytes :: from ( ok_code) ) ,
1393+ (
1394+ writer_batched,
1395+ U256 :: zero ( ) ,
1396+ 0 ,
1397+ Bytes :: from ( SSTORE_SET_THEN_CLEAR_CODE . to_vec ( ) ) ,
1398+ ) ,
1399+ (
1400+ reverter,
1401+ U256 :: zero ( ) ,
1402+ 0 ,
1403+ Bytes :: from ( PURE_REVERT_CODE . to_vec ( ) ) ,
1404+ ) ,
1405+ ] ,
1406+ tx. clone ( ) ,
1407+ ) ;
1408+ let report = result. expect ( "valid frame tx (failed batch, payer approved)" ) ;
1409+ let fr = report
1410+ . frame_results
1411+ . as_ref ( )
1412+ . expect ( "frame results present" ) ;
1413+ assert_eq ! (
1414+ ( fr[ 2 ] . 0 , fr[ 2 ] . 1 ) ,
1415+ ( ethrex_common:: types:: FRAME_RECEIPT_STATUS_FAILURE , 200_000 ) ,
1416+ "batched frame must be failed and charged its full gas_limit"
1417+ ) ;
1418+ assert_eq ! (
1419+ ( fr[ 3 ] . 0 , fr[ 3 ] . 1 ) ,
1420+ ( ethrex_common:: types:: FRAME_RECEIPT_STATUS_FAILURE , 30_000 ) ,
1421+ "batch terminator must be failed and charged its full gas_limit"
1422+ ) ;
1423+ let gross = gross_gas_used ( & tx, & report) ;
1424+ assert ! (
1425+ gross / 5 > SSTORE_RESTORE_EMPTY_REFUND ,
1426+ "test setup must keep the refund cap non-binding (gross = {gross})"
1427+ ) ;
1428+ assert_eq ! (
1429+ report. gas_used,
1430+ gross - SSTORE_RESTORE_EMPTY_REFUND ,
1431+ "the reverted batch's refund contributions must be discarded"
1432+ ) ;
1433+ }
1434+
1435+ #[ test]
1436+ fn refund_capped_at_one_fifth_of_gross_gas ( ) {
1437+ let writer = Address :: from_low_u64_be ( 0x3530 ) ;
1438+ let mut writer_code = Vec :: new ( ) ;
1439+ for slot in 0u8 ..20 {
1440+ writer_code. extend_from_slice ( & [ 0x60 , 0x01 , 0x60 , slot, 0x55 ] ) ;
1441+ writer_code. extend_from_slice ( & [ 0x60 , 0x00 , 0x60 , slot, 0x55 ] ) ;
1442+ }
1443+ writer_code. push ( 0x00 ) ;
1444+ let tx = frame_tx_with_frames ( vec ! [
1445+ verify_frame( FUNDED_SENDER ) ,
1446+ default_frame( writer, 0x00 , 1_000_000 ) ,
1447+ ] ) ;
1448+ let ( result, _db) = run_frame_tx (
1449+ & [
1450+ (
1451+ FUNDED_SENDER ,
1452+ AUTO_SEED_SENDER_BALANCE ,
1453+ 0 ,
1454+ Bytes :: from ( APPROVE_BOTH_CODE . to_vec ( ) ) ,
1455+ ) ,
1456+ ( writer, U256 :: zero ( ) , 0 , Bytes :: from ( writer_code) ) ,
1457+ ] ,
1458+ tx. clone ( ) ,
1459+ ) ;
1460+ let report = result. expect ( "valid frame tx (payer approved)" ) ;
1461+ assert ! ( matches!( report. result, TxResult :: Success ) ) ;
1462+ let gross = gross_gas_used ( & tx, & report) ;
1463+ assert ! (
1464+ 20 * SSTORE_RESTORE_EMPTY_REFUND > gross / 5 ,
1465+ "test setup must make the refund counter exceed the cap (gross = {gross})"
1466+ ) ;
1467+ assert_eq ! (
1468+ report. gas_used,
1469+ gross - gross / 5 ,
1470+ "the applied refund must be capped at gross/5"
1471+ ) ;
1472+ assert_eq ! ( report. gas_spent, report. gas_used) ;
1473+ }
1474+
12151475// ==================== frame_tx opcode handler unit tests ====================
12161476// (migrated from crates/vm/levm/src/opcode_handlers/frame_tx.rs)
12171477
0 commit comments