Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,13 @@ private static IEnumerable<TestCaseData> RoundtripCases()
Frame(),
])).SetName("Roundtrip_AllModesFlagsTargetsAndData");

yield return new TestCaseData(CreateFrameTx(frames:
[
Frame(gasLimit: 500_000, stateGasLimit: 183_600),
Frame(mode: TxFrame.ModeVerify, gasLimit: 90_000, stateGasLimit: 0),
Frame(mode: TxFrame.ModeSender, gasLimit: ulong.MaxValue - 1, stateGasLimit: 1),
])).SetName("Roundtrip_TwoDimensionalGasLimits");

yield return new TestCaseData(CreateFrameTx(signatures:
[
new TxFrameSignature(TxFrameSignature.SchemeArbitrary, null, default, FilledBytes(11, 0x77)),
Expand Down Expand Up @@ -479,7 +486,8 @@ private static void AssertFramesEqual(TxFrame[] actual, TxFrame[] expected)
Assert.That(actual[i].Mode, Is.EqualTo(expected[i].Mode), $"frame {i} mode");
Assert.That(actual[i].Flags, Is.EqualTo(expected[i].Flags), $"frame {i} flags");
Assert.That(actual[i].Target, Is.EqualTo(expected[i].Target), $"frame {i} target");
Assert.That(actual[i].GasLimit, Is.EqualTo(expected[i].GasLimit), $"frame {i} gas limit");
Assert.That(actual[i].ExecutionGasLimit, Is.EqualTo(expected[i].ExecutionGasLimit), $"frame {i} execution gas limit");
Assert.That(actual[i].StateGasLimit, Is.EqualTo(expected[i].StateGasLimit), $"frame {i} state gas limit");
Assert.That(actual[i].Value, Is.EqualTo(expected[i].Value), $"frame {i} value");
Assert.That(actual[i].Data.ToArray(), Is.EqualTo(expected[i].Data.ToArray()), $"frame {i} data");
}
Expand Down Expand Up @@ -510,8 +518,8 @@ private static Transaction CreateFrameTx(TxFrame[]? frames = null, TxFrameSignat
DecodedMaxFeePerGas = 30.GWei,
};

private static TxFrame Frame(byte mode = TxFrame.ModeDefault, byte flags = 0, Address? target = null, ulong gasLimit = 100_000, UInt256 value = default, byte[]? data = null) =>
new(mode, flags, target, gasLimit, value, data ?? Array.Empty<byte>());
private static TxFrame Frame(byte mode = TxFrame.ModeDefault, byte flags = 0, Address? target = null, ulong gasLimit = 100_000, ulong stateGasLimit = 0, UInt256 value = default, byte[]? data = null) =>
new(mode, flags, target, gasLimit, stateGasLimit, value, data ?? Array.Empty<byte>());

private static byte[] FilledBytes(int length, byte fill)
{
Expand Down
3 changes: 3 additions & 0 deletions src/Nethermind/Nethermind.Core.Test/FrameTxValidationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ private static IEnumerable<TestCaseData> ConstraintCases()
yield return Case("ExpiryFrameWithShortData_InvalidExpiryFrame",
static tx => tx.Frames = [Frame(mode: TxFrame.ModeVerify, target: Eip8141Constants.ExpiryVerifierAddress, data: new byte[Eip8141Constants.ExpiryDataLength - 1])],
FrameTxValidation.InvalidExpiryFrame);
yield return Case("ExpiryFrameWithStateGas_InvalidExpiryFrame",
static tx => tx.Frames = [new TxFrame(TxFrame.ModeVerify, flags: 0, Eip8141Constants.ExpiryVerifierAddress, 30_000, 1, UInt256.Zero, new byte[Eip8141Constants.ExpiryDataLength])],
FrameTxValidation.InvalidExpiryFrame);
yield return Case("TwoExpiryFrames_MultipleExpiryFrames",
static tx => tx.Frames = [SelfVerifyFrame(), ExpiryFrame(), ExpiryFrame()],
FrameTxValidation.MultipleExpiryFrames);
Expand Down
41 changes: 33 additions & 8 deletions src/Nethermind/Nethermind.Core/FrameTxValidation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ public static bool IsWellFormed(Transaction transaction, bool postTxEnabled, out

if (frame.Mode == TxFrame.ModeVerify && frame.Target == Eip8141Constants.ExpiryVerifierAddress)
{
if (frame.Flags != 0 || !frame.Value.IsZero || frame.Data.Length != Eip8141Constants.ExpiryDataLength)
if (frame.Flags != 0 || !frame.Value.IsZero || frame.StateGasLimit != 0 || frame.Data.Length != Eip8141Constants.ExpiryDataLength)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — new consensus rule without a spec citation.

frame.StateGasLimit != 0 is a new validity condition on the expiry verifier frame. Per .agents/rules/coding-style.md ("Non-obvious consensus rules or algorithms must reference the EIP number or Yellow Paper section"), please anchor it — the surrounding conditions predate the split and carry their justification in the enclosing docs, but this one is new. Also worth double-checking against the merged spec text whether the expiry frame is required to have limits.state == 0 specifically, or whether the spec constrains limits.execution too (in which case a fixed execution budget should be asserted here as well).

{
error = InvalidExpiryFrame;
return false;
Expand All @@ -170,8 +170,9 @@ public static bool IsWellFormed(Transaction transaction, bool postTxEnabled, out
hasExpiryFrame = true;
}

ulong accumulated = totalFrameGas + frame.GasLimit;
if (accumulated < totalFrameGas)
ulong frameGas = frame.ExecutionGasLimit + frame.StateGasLimit;
ulong accumulated = totalFrameGas + frameGas;
if (frameGas < frame.ExecutionGasLimit || accumulated < totalFrameGas)
{
error = FrameGasOverflow;
return false;
Expand Down Expand Up @@ -250,8 +251,10 @@ static bool BelongsToAtomicBatch(TxFrame[] frames, int i) =>
};

/// <summary>
/// An upper bound on the public-mempool validation work of <paramref name="transaction"/>: the gas limits
/// of its validation prefix plus the cost of verifying its signatures, saturating at <see cref="ulong.MaxValue"/>.
/// An upper bound on the public-mempool validation work of <paramref name="transaction"/>: the execution-gas
/// limits (EIP-8141 <c>MAX_VERIFY_GAS</c>) of its validation prefix plus the cost of verifying its signatures,
/// saturating at <see cref="ulong.MaxValue"/>. The prefix's <c>limits.state</c> is bounded separately by
/// <c>MAX_VERIFY_STATE_GAS</c> and does not enter this budget.
/// </summary>
/// <remarks>
/// Derived from the frame layout alone, so no state is read. Each layout of EIP-8141 "Public
Expand All @@ -269,7 +272,7 @@ public static ulong ValidationWorkGas(Transaction transaction)
ulong total = 0;
for (int i = 0; i < counted; i++)
{
total = Saturating(total, frames[i].GasLimit);
total = Saturating(total, frames[i].ExecutionGasLimit);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High — MAX_VERIFY_GAS becomes bypassable while the runtime still uses a combined pool.

This now prices the validation prefix at limits.execution only, but the runtime seeds the frame's VM pool with the combined budget (TransactionProcessorBase.FrameTx.cs:594TGasPolicy.FromULong(frame.GasLimit) where GasLimit == execution + state), and the state-gas reservoir is unchanged. So a prefix frame declaring limits = [1, 100_000_000] scores ~1 gas here, sails past FrameTxVerifyGasFilter (FrameTxMaxVerifyGas = 100_000 by default), and still gets a 100M-gas pool of arbitrary work when the pool simulates the prefix. The declared state gas does not constrain what the frame spends it on until the dual-pool seeding lands.

The affordability check is not a mitigation: MAX_VERIFY_GAS exists precisely for the case where the attacker never pays (tx never included), so a funded sender can spam unbounded free validation work.

Since both the dual-pool seeding and MAX_VERIFY_STATE_GAS are explicitly out of scope here, the two halves of the split need to stay consistent. Options:

  • keep this on the combined frames[i].GasLimit until the runtime actually seeds gas_left = limits.execution, or
  • land the MAX_VERIFY_STATE_GAS bound in this PR alongside the execution-only change.

}

foreach (TxFrameSignature signature in transaction.FrameSignatures ?? [])
Expand All @@ -280,6 +283,27 @@ public static ulong ValidationWorkGas(Transaction transaction)
return total;
}

/// <summary>
/// An upper bound on the state growth EIP-8141 admits through the public mempool for
/// <paramref name="transaction"/>: the sum of its validation prefix's <c>limits.state</c>, saturating at
/// <see cref="ulong.MaxValue"/>. Bounded separately by <c>MAX_VERIFY_STATE_GAS</c>; signature verification
/// uses no state gas, so it does not enter this budget.
/// </summary>
/// <param name="transaction">The frame transaction to price.</param>
public static ulong ValidationWorkStateGas(Transaction transaction)
{
TxFrame[] frames = transaction.Frames ?? [];
int counted = RecognizedPrefixLength(frames, transaction.SenderAddress) ?? frames.Length;

ulong total = 0;
for (int i = 0; i < counted; i++)
{
total = Saturating(total, frames[i].StateGasLimit);
}

return total;
}

/// <summary>
/// The number of leading frames forming a validation prefix EIP-8141 recognizes for the public
/// mempool, or <c>null</c> when the layout matches none of them.
Expand Down Expand Up @@ -401,8 +425,9 @@ private static bool CalculateGasBudget(Transaction transaction, IReleaseSpec spe
tokens += CountCalldataTokens(frame.Data.Span, spec);
dataLength += (ulong)frame.Data.Length;

ulong accumulated = totalFrameGas + frame.GasLimit;
if (accumulated < totalFrameGas)
ulong frameGas = frame.ExecutionGasLimit + frame.StateGasLimit;
ulong accumulated = totalFrameGas + frameGas;
if (frameGas < frame.ExecutionGasLimit || accumulated < totalFrameGas)
{
return false;
}
Expand Down
23 changes: 20 additions & 3 deletions src/Nethermind/Nethermind.Core/TxFrame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
namespace Nethermind.Core;

/// <summary>
/// A single frame of an EIP-8141 frame transaction: <c>[mode, flags, target, gas_limit, value, data]</c>.
/// A single frame of an EIP-8141 frame transaction: <c>[mode, flags, target, limits, value, data]</c>,
/// where <c>limits = [execution, state]</c>.
/// https://eips.ethereum.org/EIPS/eip-8141
/// </summary>
public class TxFrame(byte mode, byte flags, Address? target, ulong gasLimit, UInt256 value, ReadOnlyMemory<byte> data)
public class TxFrame(byte mode, byte flags, Address? target, ulong executionGasLimit, ulong stateGasLimit, UInt256 value, ReadOnlyMemory<byte> data)
{
public const byte ModeDefault = 0;
public const byte ModeVerify = 1;
Expand All @@ -26,13 +27,29 @@ public class TxFrame(byte mode, byte flags, Address? target, ulong gasLimit, UIn
public const byte ApproveScopeMask = ApproveExecutionAndPayment;
public const byte AtomicBatchFlag = 0x4;

/// <summary>Constructs a frame whose entire budget is execution gas, with <c>limits.state == 0</c>.</summary>
public TxFrame(byte mode, byte flags, Address? target, ulong gasLimit, UInt256 value, ReadOnlyMemory<byte> data)
: this(mode, flags, target, gasLimit, 0, value, data)
{
}

public byte Mode { get; } = mode;
public byte Flags { get; } = flags;

/// <summary>Null resolves to the transaction sender during execution.</summary>
public Address? Target { get; } = target;

public ulong GasLimit { get; } = gasLimit;
/// <summary>EIP-8141 <c>limits.execution</c>: the frame's execution-gas budget.</summary>
public ulong ExecutionGasLimit { get; } = executionGasLimit;

/// <summary>EIP-8141 <c>limits.state</c>: the frame's state-gas budget (EIP-8037).</summary>
public ulong StateGasLimit { get; } = stateGasLimit;

/// <summary>The combined gas the frame reserves against the payer, <c>limits.execution + limits.state</c>.</summary>
/// <remarks>Static validation rejects a transaction whose frame reservations overflow, so this sum never
/// wraps for a frame reaching execution.</remarks>
public ulong GasLimit => ExecutionGasLimit + StateGasLimit;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — GasLimit wraps silently, and it is read before validation.

The remark is true for frames that reach execution, but FrameTxDecoder.DecodeInternal reads frame.GasLimit while summing transaction.GasLimit (TxDecoders/FrameTxDecoder.cs:148) — i.e. at decode time, before IsWellFormed runs. With execution = ulong.MaxValue, state = 10 the property returns 9, so the saturating sum there is defeated and the tx is briefly visible with an absurdly small GasLimit. Validation does reject it afterwards, so there is no path to execution, but failing closed is cheap here:

Suggested change
public ulong GasLimit => ExecutionGasLimit + StateGasLimit;
public ulong GasLimit => ExecutionGasLimit > ulong.MaxValue - StateGasLimit ? ulong.MaxValue : ExecutionGasLimit + StateGasLimit;

(If you prefer to keep the plain sum, at least widen the remark to say the property may wrap for a transaction that has not yet passed static validation.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] The combined GasLimit alias collapses independent reservations

Transaction.GasLimit is populated by summing this alias, while the unchanged EIP-8037 inclusion check feeds that scalar into both dimensions and the block picker compares the combined budget with one remaining-gas value. A transaction whose execution and state reservations each fit independently can therefore be rejected solely because their sum exceeds the block limit. The same alias lets the default-code keyed-nonce surcharge treat limits.state as execution headroom, affecting both block validity and frame execution.


public UInt256 Value { get; } = value;
public ReadOnlyMemory<byte> Data { get; } = data;

Expand Down
57 changes: 57 additions & 0 deletions src/Nethermind/Nethermind.Evm.Test/FrameTxBlockGasTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,63 @@ public void Execute_PayloadFrameReverts_OwesNoStateGas()
"a reverted frame commits no state, so it grows none");
}

/// <summary>
/// A frame's state gas is drawn from its own <c>limits.state</c> reservoir, independent of
/// <c>limits.execution</c>: a fresh-slot write whose execution budget cannot absorb the state charge
/// still succeeds when the state budget covers it, and the same charge lands in the state dimension.
/// </summary>
[Test]
public void Execute_PayloadFrameStateBudgetCoversTheWrite_SucceedsFromTheStateReservoir()
{
Deploy(Sender, ApproveCode(TxFrame.ApproveExecutionAndPayment), UInt256.Parse("100000000000000000000"));
Deploy(Writer, Prepare.EvmCode.PushData(1).PushData(0).Op(Instruction.SSTORE).Op(Instruction.STOP).Done);

const ulong executionBudget = 30_000;
TestAllTracerWithOutput tracer = new();
Transaction tx = FrameTx(nonce: 0,
new TxFrame(TxFrame.ModeVerify, TxFrame.ApproveExecutionAndPayment, target: null, gasLimit: 200_000, UInt256.Zero, default),
new TxFrame(TxFrame.ModeSender, 0, Writer, executionBudget, 150_000, UInt256.Zero, default));

Assert.That(Process(tx, tracer).TransactionExecuted, Is.True);

const ulong stateCharge = (ulong)GasCostOf.SSetState;
using (Assert.EnterMultipleScope())
{
Assert.That(_state.Get(new StorageCell(Writer, (UInt256)0)).ToArray(), Is.Not.All.EqualTo((byte)0),
"the write committed, so its state gas came from the reservoir rather than out-of-gassing");
Assert.That(tracer.GasConsumedResult.BlockStateGas, Is.EqualTo(stateCharge),
"the reservoir-funded write still bills the state dimension");
Assert.That(tracer.GasConsumedResult.EffectiveBlockGas,
Is.EqualTo(tracer.GasConsumedResult.SpentGas - stateCharge));
}
}

/// <summary>
/// With no state budget the same write's state charge spills into <c>limits.execution</c>, which cannot
/// cover it, so the frame halts and commits nothing — proving the two budgets are independent pools.
/// </summary>
[Test]
public void Execute_PayloadFrameStateSpillsIntoTooSmallExecutionBudget_HaltsAndOwesNoStateGas()
{
Deploy(Sender, ApproveCode(TxFrame.ApproveExecutionAndPayment), UInt256.Parse("100000000000000000000"));
Deploy(Writer, Prepare.EvmCode.PushData(1).PushData(0).Op(Instruction.SSTORE).Op(Instruction.STOP).Done);

const ulong executionBudget = 30_000;
TestAllTracerWithOutput tracer = new();
Transaction tx = FrameTx(nonce: 0,
new TxFrame(TxFrame.ModeVerify, TxFrame.ApproveExecutionAndPayment, target: null, gasLimit: 200_000, UInt256.Zero, default),
new TxFrame(TxFrame.ModeSender, 0, Writer, executionBudget, 0, UInt256.Zero, default));

Assert.That(Process(tx, tracer).TransactionExecuted, Is.True);

using (Assert.EnterMultipleScope())
{
Assert.That(_state.Get(new StorageCell(Writer, (UInt256)0)).ToArray(), Is.All.EqualTo((byte)0),
"the write halted out of gas, so no slot was committed");
Assert.That(tracer.GasConsumedResult.BlockStateGas, Is.Zero);
}
}

/// <summary>An atomic batch whose later frame fails gives back the state gas its earlier frame owed.</summary>
/// <remarks>
/// The unroll restores the pre-batch state, so the fresh slot the first frame wrote never reaches the
Expand Down
17 changes: 17 additions & 0 deletions src/Nethermind/Nethermind.Evm.Test/FrameTxProcessorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ public void Execute_TxParamSigHash_ExposesCanonicalHash()
[TestCase((byte)0x06, 3UL, TestName = "Execute_FrameParam_AllowedScope")]
[TestCase((byte)0x07, 0UL, TestName = "Execute_FrameParam_AtomicBatch")]
[TestCase((byte)0x08, 0UL, TestName = "Execute_FrameParam_Value")]
[TestCase((byte)0x09, 0UL, TestName = "Execute_FrameParam_StateGasLimit")]
public void Execute_FrameParamIntrospection_ReadsCompletedFrame(byte param, ulong expected)
{
DeploySmartSender(ApproveCode(TxFrame.ApproveExecutionAndPayment));
Expand All @@ -511,6 +512,22 @@ public void Execute_FrameParamIntrospection_ReadsCompletedFrame(byte param, ulon
AssertStorage(Observer, 0, (UInt256)expected);
}

[Test]
public void Execute_FrameParam_StateGasLimit_ReadsDeclaredStateBudget()
{
DeploySmartSender(ApproveCode(TxFrame.ApproveExecutionAndPayment));
DeployContract(Observer, Prepare.EvmCode
.PushData(0x09).PushData(1).Op(Instruction.FRAMEPARAM).PushData(0).Op(Instruction.SSTORE)
.Op(Instruction.STOP).Done);
Transaction tx = FrameTx(nonce: 0, SelfVerifyFrame(),
new TxFrame(TxFrame.ModeDefault, 0, Observer, 200_000, 50_000, UInt256.Zero, default));

TransactionResult result = Process(tx);

Assert.That(result.TransactionExecuted, Is.True);
AssertStorage(Observer, 0, (UInt256)50_000);
}

[Test]
public void Execute_FrameParamStatusOfCurrentFrame_ExceptionallyHalts()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ public struct EthereumGasPolicy : IGasPolicy<EthereumGasPolicy>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static EthereumGasPolicy FromULong(ulong value) => new() { Value = value };

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static EthereumGasPolicy FromFrameLimits(ulong executionGasLimit, ulong stateGasLimit) =>
new() { Value = executionGasLimit, StateReservoir = (long)stateGasLimit };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] Frame state charges can spill into the execution budget

FromFrameLimits seeds the existing EIP-8037 reservoir policy, whose ConsumeStateGas deducts any reservoir shortfall from Value. EIP-8141's explicit frame pools are independent and exclude this spill behavior. A state charge above StateGasLimit therefore succeeds whenever ExecutionGasLimit covers the difference, whereas a conforming client exceptionally halts the frame; committed state and receipts then diverge.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (long)stateGasLimit is an unchecked narrowing cast: a stateGasLimit > long.MaxValue silently becomes a negative StateReservoir, and ConsumeStateGas/CalculateStateGasSpill treat a non-positive reservoir as "spill everything into execution". The sibling default IGasPolicy.FromFrameLimits handles the same overflow domain by saturating at ulong.MaxValue, so the two implementations disagree on out-of-range input.

Not reachable today (a frame reaching execution has passed affordability and block-gas-limit checks, which bound the limits far below long.MaxValue), hence a nit — but the inconsistency is a latent trap if a future caller invokes this off the execution path. Consider clamping the reservoir the same way the default does. (Distinct from the already-raised spill-semantics concern on this method.)


[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static EthereumGasPolicy CreateSystemTransactionIntrinsicGas(ulong blockGasLimit) =>
new()
Expand Down
9 changes: 9 additions & 0 deletions src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ public interface IGasPolicy<TSelf> where TSelf : struct, IGasPolicy<TSelf>
{
static abstract TSelf FromULong(ulong value);

/// <summary>
/// Seeds an EIP-8141 frame budget from its two-dimensional <c>limits = [execution, state]</c>: the execution
/// dimension funds <c>gas_left</c> and the state dimension funds the state reservoir. Pre-EIP-8037 policies,
/// having no state dimension, fall back to a single combined budget.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static virtual TSelf FromFrameLimits(ulong executionGasLimit, ulong stateGasLimit) =>
TSelf.FromULong(executionGasLimit > ulong.MaxValue - stateGasLimit ? ulong.MaxValue : executionGasLimit + stateGasLimit);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
static virtual TSelf CreateSystemTransactionIntrinsicGas(ulong blockGasLimit) => TSelf.FromULong(0);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,21 +217,22 @@ public static EvmExceptionType InstructionFrameParam<TGasPolicy, TTracingInst>(V
// Spec stack order: frameIndex on top, param second.
if (!stack.PopUInt256(out UInt256 frameIndex, out UInt256 param)) return EvmExceptionType.StackUnderflow;
if (frameIndex >= (UInt256)ctx.Frames.Length) return EvmExceptionType.BadInstruction;
if (param > 0x08) return EvmExceptionType.BadInstruction;
if (param > 0x09) return EvmExceptionType.BadInstruction;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] FRAMEPARAM rejects the required gas-usage selectors

The updated EIP-8141 table defines 0x0A and 0x0B for a completed frame's execution and state usage. This bound returns BadInstruction for both. A later frame that introspects either usage therefore exceptionally halts instead of receiving the recorded value, which can change subsequent approvals and state.


int index = (int)frameIndex.u0;
TxFrame frame = ctx.Frames[index];
return param.u0 switch
{
0x00 => stack.PushAddress<TTracingInst>(ctx.ResolvedTarget(index)),
0x01 => stack.PushUInt256<TTracingInst>((UInt256)frame.GasLimit),
0x01 => stack.PushUInt256<TTracingInst>((UInt256)frame.ExecutionGasLimit),
0x02 => stack.PushUInt32<TTracingInst>(frame.Mode),
0x03 => stack.PushUInt32<TTracingInst>(frame.Flags),
0x04 => stack.PushUInt256<TTracingInst>((UInt256)frame.Data.Length),
0x05 => FrameStatus<TTracingInst>(ctx, index, ref stack),
0x06 => stack.PushUInt32<TTracingInst>(frame.AllowedApproveScope),
0x07 => stack.PushUInt32<TTracingInst>((uint)(frame.IsAtomicBatch ? 1 : 0)),
0x08 => stack.PushUInt256<TTracingInst>(frame.Value),
0x09 => stack.PushUInt256<TTracingInst>((UInt256)frame.StateGasLimit),
_ => EvmExceptionType.BadInstruction,
};
}
Expand Down
Loading
Loading