Skip to content

Commit b1a98a9

Browse files
feat(op-revm): add SGT fee burning via collect_native_balance
When SGT is enabled and not native-backed, fee recipients should only receive the native portion of fees — the SGT portion is burned. This matches op-geth's collectNativeBalance behavior. - Add collect_native_balance to sgt.rs that splits fees between SGT (burned) and native (paid) pools - Change reward_beneficiary return type to Result<U256> so the OP handler can capture the coinbase fee amount - Apply collect_native_balance to coinbase fee and all OP-specific fees (L1, base fee, operator fee) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c1638ca commit b1a98a9

4 files changed

Lines changed: 80 additions & 8 deletions

File tree

crates/handler/src/handler.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,8 @@ pub trait Handler {
451451
evm: &mut Self::Evm,
452452
exec_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
453453
) -> Result<(), Self::Error> {
454-
post_execution::reward_beneficiary(evm.ctx(), exec_result.gas()).map_err(From::from)
454+
post_execution::reward_beneficiary(evm.ctx(), exec_result.gas())?;
455+
Ok(())
455456
}
456457

457458
/// Processes the final execution output.

crates/handler/src/post_execution.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,13 @@ pub fn reimburse_caller<CTX: ContextTr>(
5454
}
5555

5656
/// Rewards the beneficiary with transaction fees.
57+
///
58+
/// Returns the coinbase fee amount that was credited to the beneficiary.
5759
#[inline]
5860
pub fn reward_beneficiary<CTX: ContextTr>(
5961
context: &mut CTX,
6062
gas: &Gas,
61-
) -> Result<(), <CTX::Db as Database>::Error> {
63+
) -> Result<U256, <CTX::Db as Database>::Error> {
6264
let (block, tx, cfg, journal, _, _) = context.all_mut();
6365
let basefee = block.basefee() as u128;
6466
let effective_gas_price = tx.effective_gas_price(basefee);
@@ -71,12 +73,14 @@ pub fn reward_beneficiary<CTX: ContextTr>(
7173
effective_gas_price
7274
};
7375

76+
let coinbase_fee = U256::from(coinbase_gas_price * gas.used() as u128);
77+
7478
// reward beneficiary
7579
journal
7680
.load_account_mut(block.beneficiary())?
77-
.incr_balance(U256::from(coinbase_gas_price * gas.used() as u128));
81+
.incr_balance(coinbase_fee);
7882

79-
Ok(())
83+
Ok(coinbase_fee)
8084
}
8185

8286
/// Calculate last gas spent and transform internal reason to external.

crates/op-revm/src/handler.rs

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
use crate::{
33
api::exec::OpContextTr,
44
constants::{BASE_FEE_RECIPIENT, L1_FEE_RECIPIENT, OPERATOR_FEE_RECIPIENT},
5-
sgt::{add_sgt_balance, deduct_sgt_balance, read_sgt_balance},
5+
sgt::{add_sgt_balance, collect_native_balance, deduct_sgt_balance, read_sgt_balance},
66
transaction::{deposit::DEPOSIT_TRANSACTION_TYPE, OpTransactionError, OpTxTr},
77
L1BlockInfo, OpHaltReason, OpSpecId,
88
};
@@ -430,7 +430,32 @@ where
430430
return Ok(());
431431
}
432432

433-
self.mainnet.reward_beneficiary(evm, frame_result)?;
433+
// Call post_execution::reward_beneficiary directly to get the coinbase fee amount
434+
let coinbase_fee = post_execution::reward_beneficiary(evm.ctx(), frame_result.gas())
435+
.map_err(|e| ERROR::from(ContextError::Db(e)))?;
436+
437+
let is_sgt = evm.ctx().cfg().is_sgt_enabled();
438+
let is_native_backed = evm.ctx().cfg().is_sgt_native_backed();
439+
440+
// SGT: burn the non-native portion of the coinbase fee
441+
if is_sgt {
442+
let chain = evm.ctx().chain_mut();
443+
let actual = collect_native_balance(
444+
coinbase_fee,
445+
is_native_backed,
446+
&mut chain.sgt_amount_deducted,
447+
&mut chain.sgt_native_deducted,
448+
);
449+
let burned = coinbase_fee.saturating_sub(actual);
450+
if !burned.is_zero() {
451+
let beneficiary = evm.ctx().block().beneficiary();
452+
evm.ctx()
453+
.journal_mut()
454+
.load_account_mut(beneficiary)?
455+
.decr_balance(burned);
456+
}
457+
}
458+
434459
let basefee = evm.ctx().block().basefee() as u128;
435460

436461
// If the transaction is not a deposit transaction, fees are paid out
@@ -458,13 +483,24 @@ where
458483
};
459484
let base_fee_amount = U256::from(basefee.saturating_mul(frame_result.gas().used() as u128));
460485

461-
// Send fees to their respective recipients
486+
// Send fees to their respective recipients, applying SGT burning if enabled
462487
for (recipient, amount) in [
463488
(L1_FEE_RECIPIENT, l1_cost),
464489
(BASE_FEE_RECIPIENT, base_fee_amount),
465490
(OPERATOR_FEE_RECIPIENT, operator_fee_cost),
466491
] {
467-
ctx.journal_mut().balance_incr(recipient, amount)?;
492+
let actual = if is_sgt {
493+
let chain = ctx.chain_mut();
494+
collect_native_balance(
495+
amount,
496+
is_native_backed,
497+
&mut chain.sgt_amount_deducted,
498+
&mut chain.sgt_native_deducted,
499+
)
500+
} else {
501+
amount
502+
};
503+
ctx.journal_mut().balance_incr(recipient, actual)?;
468504
}
469505

470506
Ok(())

crates/op-revm/src/sgt.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,37 @@ where
6767
Ok(())
6868
}
6969

70+
/// Collect native balance from a fee amount, burning the SGT portion.
71+
///
72+
/// When SGT is enabled and not native-backed, fees are split between SGT and native pools.
73+
/// The SGT portion is burned (not paid to recipient), while the native portion goes to the
74+
/// recipient. This matches op-geth's `collectNativeBalance`.
75+
///
76+
/// Deducts from `sgt_remaining` first (burned), then from `native_remaining` (to recipient).
77+
/// Both pools are mutated in place. Returns the native amount that should be paid to the recipient.
78+
///
79+
/// Returns `amount` unchanged when `sgt_remaining == 0` or `is_native_backed`.
80+
pub fn collect_native_balance(
81+
amount: U256,
82+
is_native_backed: bool,
83+
sgt_remaining: &mut U256,
84+
native_remaining: &mut U256,
85+
) -> U256 {
86+
if is_native_backed || sgt_remaining.is_zero() {
87+
return amount;
88+
}
89+
90+
// Burn from SGT pool first
91+
let sgt_burn = amount.min(*sgt_remaining);
92+
*sgt_remaining = sgt_remaining.saturating_sub(sgt_burn);
93+
94+
// Remainder comes from native pool
95+
let native_part = amount.saturating_sub(sgt_burn).min(*native_remaining);
96+
*native_remaining = native_remaining.saturating_sub(native_part);
97+
98+
native_part
99+
}
100+
70101
/// Add amount to SGT balance in contract storage.
71102
///
72103
/// This performs: `balance[account] += amount` in SGT contract storage.

0 commit comments

Comments
 (0)