Skip to content

Commit 1dabc40

Browse files
nonsenseclaude
andcommitted
alloy-op-evm: pin post-exec settlement conservation at arithmetic extremes
Closes the max-input saturation gap the SDM FMA flags as NON-BLOCKING under FM2. Nothing pinned that the saturating arithmetic in post_exec_settlement_deltas cannot silently truncate a settlement leg and break value conservation under the widest inputs a hostile producer can reach. Sweeps base_fee in {0, 1, 1e9, u64::MAX} x refund in {1, 21k, u64::MAX/2, u64::MAX} x effective_gas_price in {basefee, basefee+1, u128::MAX}, asserting each leg against its exact closed form as well as the conservation identity sender == beneficiary + base_fee + operator_fee. Checking the legs individually matters: the identity alone still holds if two legs clamp by offsetting amounts. All of them hold exactly, so no saturation engages — the widest product is ~2^192, well inside U256. Only effective_gas_price >= basefee is exercised: below-basefee txs never reach settlement (revm rejects them with GasPriceLessThanBasefee upstream), which is the precondition the beneficiary_gas_price saturating_sub relies on. This is coverage only; no behavior change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 3d10b89 commit 1dabc40

1 file changed

Lines changed: 55 additions & 0 deletions

File tree

rust/alloy-op-evm/src/block/tests.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,61 @@ mod sdm {
845845
);
846846
}
847847

848+
// Closes the max-input saturation gap the FMA flags under FM2. The settlement identity
849+
// `sender_delta == beneficiary_delta + base_fee_delta + operator_fee_delta` must hold
850+
// *exactly* at the arithmetic extremes a hostile producer can reach, so that no
851+
// `saturating_mul`/`saturating_add` in the deltas path can mask a non-conserving delta and
852+
// mint ETH. `refund` is bounded by `evm_gas_used` (u64) and prices by u128, so the widest
853+
// product is ~2^192 — inside U256, and therefore never actually saturating.
854+
//
855+
// Only `effective_gas_price >= basefee` is exercised: below-basefee txs cannot reach
856+
// settlement (revm rejects them upstream with GasPriceLessThanBasefee), and the
857+
// `beneficiary_gas_price` saturating_sub relies on exactly that.
858+
#[test]
859+
fn test_post_exec_settlement_conserves_value_at_arithmetic_extremes() {
860+
for base_fee in [0u64, 1, 1_000_000_000, u64::MAX] {
861+
let mut fixture = SDMExecutorFixture { base_fee, ..Default::default() };
862+
let mut executor = fixture.executor();
863+
864+
for refund in [1u64, 21_000, u64::MAX / 2, u64::MAX] {
865+
for gas_price in [u128::from(base_fee), u128::from(base_fee) + 1, u128::MAX] {
866+
let tx = legacy_tx_with_price(0, Address::from([0x11; 20]), 50_000, gas_price);
867+
let deltas = executor
868+
.post_exec_settlement_deltas(&tx, u64::MAX, refund, false, false)
869+
.expect("settlement deltas");
870+
let inputs =
871+
format!("base_fee={base_fee}, refund={refund}, gas_price={gas_price}");
872+
873+
// Each leg must equal its exact closed form: the conservation identity alone
874+
// would still hold if two legs clamped by offsetting amounts.
875+
let refund_u256 = U256::from(refund);
876+
assert_eq!(
877+
deltas.base_fee_balance_delta,
878+
refund_u256 * U256::from(base_fee),
879+
"base-fee leg must not saturate ({inputs})",
880+
);
881+
assert_eq!(
882+
deltas.beneficiary_balance_delta,
883+
refund_u256 * U256::from(gas_price - u128::from(base_fee)),
884+
"beneficiary leg must not saturate ({inputs})",
885+
);
886+
assert_eq!(
887+
deltas.sender_balance_delta,
888+
refund_u256 * U256::from(gas_price) + deltas.operator_fee_balance_delta,
889+
"sender credit must not saturate ({inputs})",
890+
);
891+
assert_eq!(
892+
deltas.sender_balance_delta,
893+
deltas.beneficiary_balance_delta +
894+
deltas.base_fee_balance_delta +
895+
deltas.operator_fee_balance_delta,
896+
"settlement must conserve value ({inputs})",
897+
);
898+
}
899+
}
900+
}
901+
}
902+
848903
// A settlement debit larger than a recipient's balance invalidates the block via
849904
// PostExecSettlementUnderflow rather than silently saturating — this is the guard against a
850905
// malformed/adversarial payload minting ETH out of an underfunded vault.

0 commit comments

Comments
 (0)