Skip to content
Open
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
6 changes: 3 additions & 3 deletions src/Nethermind/Chains/xdc-testnet.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@
"TimeoutSyncThreshold": 3,
"TimeoutPeriod": 10,
"MinePeriod": 2,
"MasternodeReward": 63420000000000001704,
"ProtectorReward": 50270000000000003128,
"ObserverReward": 25129999999999999006,
"MasternodeReward": 63.42,
"ProtectorReward": 50.27,
"ObserverReward": 25.13,
"MinimumMinerBlockPerEpoch": 5,
"LimitPenaltyEpoch": 5,
"MinimumSigningTx": 30
Expand Down
174 changes: 174 additions & 0 deletions src/Nethermind/Nethermind.Xdc.Test/XdcChainSpecRewardTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System.IO;
using System.Text;
using Nethermind.Int256;
using Nethermind.Logging;
using Nethermind.Serialization.Json;
using Nethermind.Specs.ChainSpecStyle;
using Nethermind.Xdc.RPC;
using Nethermind.Xdc.Spec;
using NUnit.Framework;

namespace Nethermind.Xdc.Test;

/// <summary>
/// Covers reading XDPoS rewards, which the chainspec states in XDC, and the boundary that keeps
/// that unit convention inside <c>engine.XDPoS.params</c>.
/// </summary>
[TestFixture, Parallelizable(ParallelScope.All)]
public class XdcChainSpecRewardTests
{
/// <summary>The only Apothem v2 config that sets rewards.</summary>
private const ulong ApothemRewardSwitchRound = 27_360_000;

private const string RewardPlaceholder = "$REWARD$";
private const string CoreParamsPlaceholder = "$CORE_PARAMS$";
private const string EnginePeriodPlaceholder = "$PERIOD$";

private const string ChainSpecTemplate = $$"""
{
"name": "xdc-reward-test",
"engine": {
"XDPoS": {
"params": {
"period": {{EnginePeriodPlaceholder}},
"epoch": 900,
"masternodeReward": {{RewardPlaceholder}},
"v2Configs": [
{
"SwitchRound": 0,
"MasternodeReward": {{RewardPlaceholder}}
}
]
}
}
},
"params": { "chainId": 50{{CoreParamsPlaceholder}} }
}
""";

/// <remarks>
/// The three Apothem amounts are the ones that pin the conversion: the reference scales the
/// float64 through a 64-bit significand and truncates, so 63.42 XDC is 63420000000000001704 wei
/// rather than a round 6.342e19. Getting this wrong changes what validators are paid.
/// </remarks>
[TestCase("63.42", "63420000000000001704", TestName = "Masternode reward")]
[TestCase("50.27", "50270000000000003128", TestName = "Protector reward")]
[TestCase("25.13", "25129999999999999006", TestName = "Observer reward")]
[TestCase("6.342e1", "63420000000000001704", TestName = "Exponent notation is the same amount")]
[TestCase("1", "1000000000000000000", TestName = "Whole XDC")]
[TestCase("2.5", "2500000000000000000", TestName = "Exactly representable fraction")]
[TestCase("0.5", "500000000000000000", TestName = "Fraction below one")]
[TestCase("1e-18", "1", TestName = "One wei")]
[TestCase("1e-19", "0", TestName = "Below one wei truncates to zero")]
[TestCase("0", "0", TestName = "Zero")]
[TestCase("0.0", "0", TestName = "Zero with a fraction")]
public void Reward_stated_in_xdc_is_converted_to_wei(string literal, string expectedWei)
{
XdcChainSpecEngineParameters parameters = LoadEngineParameters(literal);

UInt256 wei = UInt256.Parse(expectedWei);
Assert.Multiple(() =>
{
Assert.That(parameters.MasternodeReward, Is.EqualTo(wei));
Assert.That(parameters.V2Configs[0].MasternodeReward, Is.EqualTo(wei));
});
}

[TestCase("-1", TestName = "Negative")]
[TestCase("-0.0", TestName = "Negative zero")]
[TestCase("1e60", TestName = "Beyond UInt256 once scaled")]
[TestCase("\"0x37f0e6c9e9dd0e0000\"", TestName = "Hex string, which would have to mean wei")]
[TestCase("\"63.42\"", TestName = "Quoted amount")]
public void Reward_that_is_not_an_xdc_amount_is_rejected(string literal) =>
Assert.That(() => LoadEngineParameters(literal), Throws.TypeOf<InvalidDataException>());
Comment on lines +80 to +86

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.

Medium — the rejection cases don't cover the one input the migration actually produces.

1e60 only proves the UInt256 ceiling works. The realistic bad input is an operator chainspec that still states the reward in wei — literally the value this PR deletes from xdc-testnet.json:

[TestCase("63420000000000001704", TestName = "Reward still stated in wei")]

Today that case would fail, because 6.342e37 wei fits in a UInt256 and loads silently (see the comment on XdcToWeiConverter.cs). It's worth adding alongside whatever bound you settle on, so the test documents the migration boundary rather than just the arithmetic one.

Two more gaps worth closing while you're here:

  • The write path is untested, which is why the XDPoS_networkInformation breakage got through. A test that serializes a V2ConfigParams (or the whole NetworkInformation response) would have caught it.
  • Shipped_testnet_rewards_are_unchanged reads V2Configs[^1] with the comment "the newest config … sorts last by switch round". That's true, but the assertion is only meaningful because that entry happens to be the one carrying rewards. If a later config is appended without reward fields, this silently starts asserting three zeros against three non-zero expectations — it'd fail, so it's safe, just confusingly. Selecting by SwitchRound would say what the test means.


/// <summary>
/// The conversion is opt-in per property, so an XDPoS field that was not annotated keeps the
/// shared converter's behaviour.
/// </summary>
[Test]
public void Fractional_value_in_an_unannotated_engine_field_is_rejected() =>
Assert.That(() => LoadEngineParameters("1", period: "2.0"), Throws.TypeOf<InvalidDataException>());

/// <summary>
/// The converter is attached to properties owned by <c>Nethermind.Xdc</c>, so nothing outside the
/// XDPoS engine section gains either the XDC unit or tolerance for a fractional number.
/// </summary>
[TestCase(", \"eip150Transition\": 2.0", TestName = "Transition block")]
[TestCase(", \"terminalTotalDifficulty\": 1e18", TestName = "Terminal total difficulty")]
public void Fractional_value_outside_the_engine_section_is_rejected(string coreParams) =>
Assert.That(() => LoadEngineParameters("1", coreParams: coreParams), Throws.TypeOf<InvalidDataException>());

/// <summary>
/// Pins the shipped Apothem chainspec to the wei amounts it resolved to before the rewards were
/// restated in XDC.
/// </summary>
[Test]
public void Shipped_testnet_rewards_are_unchanged()
{
V2ConfigParams config = LoadShippedTestnetRewardConfig();

Assert.Multiple(() =>
{
Assert.That(config.MasternodeReward, Is.EqualTo(UInt256.Parse("63420000000000001704")));
Assert.That(config.ProtectorReward, Is.EqualTo(UInt256.Parse("50270000000000003128")));
Assert.That(config.ObserverReward, Is.EqualTo(UInt256.Parse("25129999999999999006")));
});
}

/// <summary>
/// <see cref="V2ConfigParams"/> doubles as the <c>XDPoS_networkInformation</c> response DTO, so
/// the reward properties are serialized as well as read. The response reports wei, unchanged by
/// the chainspec moving to XDC.
/// </summary>
[Test]
public void Network_information_response_reports_rewards_in_wei()
{
NetworkInformation response = new()
{
ConsensusConfigs = new XDPoSConfig { V2Configs = [LoadShippedTestnetRewardConfig()] }
};

string json = new EthereumJsonSerializer().Serialize(response);

// The three rewards as the QUANTITY the endpoint has always emitted.
Assert.Multiple(() =>
{
Assert.That(json, Does.Contain("0x3702119fc874606a8"));
Assert.That(json, Does.Contain("0x2b9a2eaa87ae30c38"));
Assert.That(json, Does.Contain("0x15cbfb1db0590fc1e"));
});
}

private static V2ConfigParams LoadShippedTestnetRewardConfig()
{
ChainSpec chainSpec = new ChainSpecFileLoader(new EthereumJsonSerializer(), LimboLogs.Instance)
.LoadEmbeddedOrFromFile("chainspec/xdc-testnet.json");

XdcChainSpecEngineParameters parameters = chainSpec.EngineChainSpecParametersProvider
.GetChainSpecParameters<XdcChainSpecEngineParameters>();

foreach (V2ConfigParams config in parameters.V2Configs)
{
if (config.SwitchRound == ApothemRewardSwitchRound) return config;
}

throw new AssertionException($"No v2 config at switch round {ApothemRewardSwitchRound}");
}

private static XdcChainSpecEngineParameters LoadEngineParameters(string reward, string coreParams = "", string period = "2")
{
string json = ChainSpecTemplate
.Replace(RewardPlaceholder, reward)
.Replace(CoreParamsPlaceholder, coreParams)
.Replace(EnginePeriodPlaceholder, period);

using MemoryStream stream = new(Encoding.UTF8.GetBytes(json));
ChainSpec chainSpec = new ChainSpecLoader(new EthereumJsonSerializer(), LimboLogs.Instance).Load(stream);

return chainSpec.EngineChainSpecParametersProvider.GetChainSpecParameters<XdcChainSpecEngineParameters>();
}
}
4 changes: 2 additions & 2 deletions src/Nethermind/Nethermind.Xdc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ transactions observed two epochs back (blocks at heights that are multiples of `

- **Pre-`TIPUpgradeReward`** — `Reward` XDC for the epoch, split proportionally to each masternode's signing
count.
- **Post-`TIPUpgradeReward`** — fixed `MasternodeReward` / `ProtectorReward` / `ObserverReward` (Wei) per
- **Post-`TIPUpgradeReward`** — fixed `MasternodeReward` / `ProtectorReward` / `ObserverReward` (XDC) per
qualifying signer, with minted and burned totals reported to the minted-record contract
([`IMintedRecordContract`](Contracts/IMintedRecordContract.cs)).

Expand Down Expand Up @@ -557,7 +557,7 @@ either fails to load.
| `TimeoutPeriod` | **seconds** | Round timeout before a timeout vote is broadcast |
| `TimeoutSyncThreshold` | count | Broadcast `SyncInfo` after this many consecutive timeouts |
| `MinePeriod` | **seconds** | Minimum spacing between a parent block and its child. `2` |
| `MasternodeReward` / `ProtectorReward` / `ObserverReward` | Wei | Fixed per-signer epoch rewards (post-`TIPUpgradeReward`) |
| `MasternodeReward` / `ProtectorReward` / `ObserverReward` | XDC | Fixed per-signer epoch rewards (post-`TIPUpgradeReward`). Stated in XDC, as in the reference client, and scaled to wei on load. `63.42` on Apothem |
| `MinimumMinerBlockPerEpoch` | blocks | Below this, a masternode is penalised. Only honoured once `TIPUpgradePenalty` is active; before that a hard-coded `1` applies |
| `LimitPenaltyEpoch` | epochs | Penalty duration used post-`TIPUpgradePenalty` |
| `MinimumSigningTx` | count | Signing transactions needed to leave penalty |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text.Json.Serialization;
using Nethermind.Core;
using Nethermind.Int256;
using Nethermind.Specs;
Expand Down Expand Up @@ -68,8 +69,11 @@ public List<V2ConfigParams> V2Configs

public ulong? TipUpgradePenalty { get; set; }
public ulong? TipUpgradeReward { get; set; }
[JsonConverter(typeof(XdcToWeiConverter))]
public UInt256 MasternodeReward { get; set; }
[JsonConverter(typeof(XdcToWeiConverter))]
public UInt256 ProtectorReward { get; set; }
[JsonConverter(typeof(XdcToWeiConverter))]
public UInt256 ObserverReward { get; set; }
public ulong MergeSignRange { get; set; }
public Address[] BlackListedAddresses { get; set; }
Expand Down Expand Up @@ -122,8 +126,11 @@ public sealed class V2ConfigParams
public int TimeoutSyncThreshold { get; init; }
public int TimeoutPeriod { get; init; }
public ulong MinePeriod { get; init; }
[JsonConverter(typeof(XdcToWeiConverter))]
public UInt256 MasternodeReward { get; init; }
[JsonConverter(typeof(XdcToWeiConverter))]
public UInt256 ProtectorReward { get; init; }
[JsonConverter(typeof(XdcToWeiConverter))]
public UInt256 ObserverReward { get; init; }
Comment on lines +129 to 134

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 — this breaks the XDPoS_networkInformation JSON-RPC method.

V2ConfigParams is not only a chainspec type; it is reused verbatim as an RPC response DTO:

A property-level [JsonConverter] applies on write too, so serializing the XDPoS_networkInformation response now calls XdcToWeiConverter.Write and throws NotSupportedException. Because JSON-RPC serializes straight into the response stream, this doesn't just fail the call cleanly — the exception lands mid-object, after NetworkInformation/XDPoSConfig have already been partially written.

The PR description states "Nothing serializes engine parameters"; that is the assumption that doesn't hold here.

Before this PR the endpoint emitted these three fields through the default UInt256 converter. Options, roughly in the order the repo guidelines prefer:

  1. Make Write reproduce the previous output (delegate to the default UInt256 handling) rather than throwing. The read/write asymmetry is real but confined, and it keeps the wire format unchanged.
  2. Give the RPC path its own DTO so the chainspec unit convention never reaches a response type.

Either way this needs a regression test — Nethermind.Xdc.Test currently has no coverage of XDPoS_networkInformation at all, which is why 590/590 passed.

Fix this →

public ulong MinimumMinerBlockPerEpoch { get; init; }
public ulong LimitPenaltyEpoch { get; init; }
Expand Down
147 changes: 147 additions & 0 deletions src/Nethermind/Nethermind.Xdc/Spec/XdcToWeiConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System;
using System.Buffers;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Nethermind.Int256;
using Nethermind.Serialization.Json;

namespace Nethermind.Xdc.Spec;

/// <summary>
/// Reads a reward stated in XDC — <c>63.42</c>, <c>1</c>, <c>2.5e1</c> — and yields the amount in wei.
/// </summary>
/// <remarks>
/// The reference client decodes these fields into a float64 and scales them by 10^18 through a
/// <c>big.Float</c> whose precision defaults to 64 bits, then truncates toward zero. Both steps are
/// load-bearing: <c>63.42</c> has to produce 63420000000000001704 — the value Apothem's genesis was
/// generated with — and neither <see cref="double"/> nor <see cref="decimal"/> arithmetic lands
/// there. So the exact product is formed in <see cref="BigInteger"/> and rounded explicitly.
/// <para>
/// The 64 comes from Go raising a zero-precision <c>big.Float</c> to that width, which only survives
/// into the product while the receiver's precision is unset — the detail that would silently change
/// the result if the reference restructured that expression. <c>Reward</c>, the pre-<c>TIPUpgradeReward</c>
/// equivalent, is likewise a whole-XDC value scaled by <c>Unit.Ether</c> in <see cref="XdcRewardCalculator"/>.
/// </para>
/// <para>
/// Only a JSON number is accepted. A hex QUANTITY would have to mean wei, and carrying two units on
/// one field is how a reward ends up wrong by a factor of 10^18.
/// </para>
/// <para>
/// Applying this per property keeps the XDC unit convention inside <c>engine.XDPoS.params</c>. The
/// converters registered on <see cref="EthereumJsonSerializer"/> are untouched, so the rest of the
/// chainspec — and JSON-RPC, where EIP-1474 requires a QUANTITY — still rejects a fractional number.
/// </para>
/// </remarks>
Comment on lines +20 to +41

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.

The derivation checks out — I reproduced all three Apothem amounts by hand. Recording the working here since the PR asks for a second pair of eyes on it, and since it's the part no reviewer can check by reading the reference format alone.

For each literal: take the exact double, form the exact product with 10^18, round to a 64-bit significand (half-to-even), truncate.

Literal double significand (exact real value) Exact product ulp @ 64 bits Rounded → truncated
63.42 8925571511494902 / 2⁴⁷ (exact: …901.76) 63420000000000001705.30 4 (2⁶⁵ ≤ x < 2⁶⁶) …1704
50.27 7074873539622339 / 2⁴⁷ (exact: …338.56) 50270000000000003126.39 4 …3128
25.13 7073466164738785 / 2⁴⁸ (exact: …785.28) 25129999999999999005.24 2 (2⁶⁴ ≤ x < 2⁶⁵) …9006

All three land exactly on the values Apothem's genesis carries. The two nearby hypotheses both fail, so the rule is pinned rather than merely consistent:

  • Truncate the exact product, no intermediate rounding…1705 / …3126 / …9005. Wrong on all three.
  • Round to 53 bits (what you'd get if the Go receiver already had prec == 53, i.e. bigval.Mul(bigval, coin) rather than new(big.Float).Mul(...)) → ulp is 8192 at 6.342e19, and 63420000000000000000 happens to be an exact multiple of 8192, so 63.42 collapses to the round value. Wrong.

Note the sign structure is what makes this convincing: two amounts round up past the round value and one rounds down, and the offsets (+1704, +3128, −994) aren't a common relative factor — they track the individual double representation error of each literal. That's hard to hit by accident.

Two small things on the comment itself:

  • ReferencePrecisionBits = 64 is the load-bearing constant and its provenance is "the three Apothem values are consistent with it". Worth naming the reference source (repo/file/function) in the <remarks> so a future reader can confirm rather than re-derive. In Go terms the 64 comes from SetInt raising a zero-precision big.Float to 64, and it only survives into the product if the receiver's precision is unset — which is exactly the detail that would silently change the answer if the reference ever restructured that expression.
  • Line 24 says the multiplier "carr[ies] the 64-bit significand its wei multiplier is built with", but 10¹⁸ needs only 60 bits — the 64 is a floor, not a fit. Slightly clearer as "whose precision defaults to 64 bits".

public sealed class XdcToWeiConverter : JsonConverter<UInt256>
{
private const int WeiPerXdcExponent = 18;

/// <summary>Significand width the reference conversion rounds to.</summary>
private const int ReferencePrecisionBits = 64;

private const int UInt256ByteCount = 32;

private static readonly UInt256Converter WeiConverter = new();

public override UInt256 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.Number) ThrowNotANumber(reader.TokenType);

if (!reader.TryGetDouble(out double amount) || !double.IsFinite(amount) || double.IsNegative(amount))
{
ThrowNotAnAmount(in reader);
}

return ToWei(amount, in reader);

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] Legacy wei-denominated XDC chainspecs are silently rescaled

Before this PR, these UInt256 fields and the module documentation treated numeric values as wei. An existing custom XDPoS or subnet chainspec can therefore contain 63420000000000001704; this path now treats it as XDC and produces roughly 6.342e37 wei, which still fits UInt256, so startup succeeds. Once TIPUpgradeReward is active, that value is minted directly, causing approximately 10^18-overpayments and a state-root split from nodes using the previous interpretation. A migration discriminator or explicit rejection of the legacy representation would prevent the silent unit change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There is no production configs out there, so this is not a problem

}

/// <remarks>
/// Writes wei rather than XDC. <see cref="V2ConfigParams"/> doubles as the
/// <c>XDPoS_networkInformation</c> response DTO, whose wire format must not change, and reporting
/// wei over JSON-RPC while configuring XDC in the chainspec is the split used everywhere else.
/// The consequence is that a <see cref="V2ConfigParams"/> does not survive a JSON round trip;
/// nothing performs one.
/// </remarks>
public override void Write(Utf8JsonWriter writer, UInt256 value, JsonSerializerOptions options) =>
WeiConverter.Write(writer, value, options);

private static UInt256 ToWei(double amount, in Utf8JsonReader reader)
{
if (amount == 0) return default;

(BigInteger significand, int exponent) = Decompose(amount);

// The amount is `significand * 2^exponent`, so its wei value is `numerator / 2^shift` exactly.
BigInteger numerator = significand * BigInteger.Pow(10, WeiPerXdcExponent);
int shift = -exponent;
if (shift < 0)
{
numerator <<= -shift;
shift = 0;
}

// A value in [2^(e-1), 2^e) keeps bits down to 2^(e-64); `numerator`'s bit length minus the
// shift is that e, because the divisor is a power of two.
int ulpExponent = (int)numerator.GetBitLength() - shift - ReferencePrecisionBits;

BigInteger rounded = ShiftRoundHalfEven(numerator, shift + ulpExponent);
BigInteger wei = ulpExponent >= 0 ? rounded << ulpExponent : rounded >> -ulpExponent;

int byteCount = wei.GetByteCount(isUnsigned: true);
if (byteCount > UInt256ByteCount) ThrowTooLarge(in reader);

Span<byte> bytes = stackalloc byte[UInt256ByteCount];
wei.TryWriteBytes(bytes[(UInt256ByteCount - byteCount)..], out _, isUnsigned: true, isBigEndian: true);

ReadOnlySpan<byte> bigEndian = bytes;
return new UInt256(in bigEndian, isBigEndian: true);
}

/// <summary>Splits a positive finite <paramref name="value"/> into <c>significand * 2^exponent</c>.</summary>
private static (BigInteger Significand, int Exponent) Decompose(double value)
{
long bits = BitConverter.DoubleToInt64Bits(value);
int biasedExponent = (int)((bits >> 52) & 0x7FF);
long significand = bits & 0xF_FFFF_FFFF_FFFF;

return biasedExponent == 0
? (significand, -1074)
: (significand | (1L << 52), biasedExponent - 1075);
}

/// <summary>Divides by <c>2^shift</c>, rounding half to even; a negative shift multiplies instead.</summary>
private static BigInteger ShiftRoundHalfEven(BigInteger value, int shift)
{
if (shift <= 0) return value << -shift;

BigInteger quotient = value >> shift;
BigInteger remainder = value - (quotient << shift);
int comparedToHalf = remainder.CompareTo(BigInteger.One << (shift - 1));

return comparedToHalf > 0 || (comparedToHalf == 0 && !quotient.IsEven)
? quotient + BigInteger.One
: quotient;
}

private static string Literal(in Utf8JsonReader reader) =>
Encoding.UTF8.GetString(reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan);

[DoesNotReturn, StackTraceHidden]
private static void ThrowNotANumber(JsonTokenType tokenType) =>
throw new JsonException($"An XDC reward must be a JSON number, found {tokenType}");

[DoesNotReturn, StackTraceHidden]
private static void ThrowNotAnAmount(in Utf8JsonReader reader) =>
throw new JsonException($"'{Literal(in reader)}' is not an XDC amount");

[DoesNotReturn, StackTraceHidden]
private static void ThrowTooLarge(in Utf8JsonReader reader) =>
throw new JsonException($"'{Literal(in reader)}' XDC exceeds the largest amount representable in wei");

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 — the message names the wrong ceiling, and nothing pins it.

wei is bounded by UInt256, not by wei as a unit — an amount larger than this is perfectly representable in wei, just not in 256 bits. Exact wording would be "…exceeds the largest amount representable as a UInt256 of wei".

Related: the only case exercising this guard is 1e60, a full order of magnitude past the ceiling ((2^256 − 1) / 10^18 ≈ 1.1579e59 XDC). It proves the guard fires, but a pair straddling the boundary would pin where it fires. Optional — the ceiling isn't a value anyone configures, and the guard's real job here is to stop the oversized wei.TryWriteBytes into bytes[(32 - byteCount)..] on line 101, which it does correctly by sitting before it.

}
Loading