Skip to content
Merged
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
8 changes: 4 additions & 4 deletions x/uexecutor/types/inbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package types
import (
"encoding/json"
"fmt"
"math/big"
"strings"

"cosmossdk.io/errors"
Expand Down Expand Up @@ -128,9 +127,10 @@ func (p Inbound) ValidateForExecution() error {
if strings.TrimSpace(p.Amount) == "" {
return errors.Wrap(sdkerrors.ErrInvalidRequest, "amount cannot be empty")
}
bi, ok := new(big.Int).SetString(p.Amount, 10)
if !ok || bi.Sign() < 0 {
return errors.Wrap(sdkerrors.ErrInvalidRequest, "amount must be a valid non-negative uint256")
// Length-capped, range-checked uint256 parse — see F-2026-18798.
bi, err := ValidateUint256String(p.Amount, "amount must be a valid non-negative uint256")
if err != nil {
return err
}
// Only GAS_AND_PAYLOAD and FUNDS_AND_PAYLOAD allow zero amount (skip deposit, still execute payload)
if bi.Sign() == 0 && p.TxType != TxType_GAS_AND_PAYLOAD && p.TxType != TxType_FUNDS_AND_PAYLOAD {
Expand Down
7 changes: 6 additions & 1 deletion x/uexecutor/types/outbound_tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,12 @@ func (p OutboundTx) ValidateBasic() error {
if strings.TrimSpace(p.Amount) == "" {
return errors.Wrap(sdkerrors.ErrInvalidRequest, "amount cannot be empty for funds tx")
}
if bi, ok := new(big.Int).SetString(p.Amount, 10); !ok || bi.Sign() <= 0 {
// Length-capped, range-checked uint256 parse — see F-2026-18798.
bi, err := ValidateUint256String(p.Amount, "amount must be a valid positive uint256")
if err != nil {
return err
}
if bi.Sign() <= 0 {
return errors.Wrap(sdkerrors.ErrInvalidRequest, "amount must be a valid positive uint256")
}
}
Expand Down
65 changes: 65 additions & 0 deletions x/uexecutor/types/uint256.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package types

import (
"math/big"

"cosmossdk.io/errors"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)

const (
// MaxUint256Bits is the width of a Solidity uint256. Anything wider cannot be
// ABI-encoded faithfully: go-ethereum's encoder truncates mod 2^256 *silently*,
// so an over-range field would make the UEA execute a value different from the
// one the user signed over.
MaxUint256Bits = 256

// MaxUint256DecimalLen caps the decimal string length accepted for a uint256
// field. 2^256-1 is exactly 78 digits; 80 leaves slack for clients that
// zero-pad. It is a cheap pre-filter, not the range check — see
// ValidateUint256String.
MaxUint256DecimalLen = 80
)

// ValidateUint256String parses value as a base-10 uint256 and returns it.
//
// The order of the three checks is load-bearing (audit finding F-2026-18798):
//
// 1. Length cap FIRST, before big.Int.SetString. big.Int decimal parsing is
// superlinear in the digit count — as reported in the finding: 78 digits
// 18µs · 100k 24.2ms · 400k 486.6ms · 900k 3.353s. This runs in
// ValidateBasic, which BaseApp executes via validateBasicTxMsgs *before* the
// ante handler, on messages that are gasless — so the work is free and
// unmetered to the attacker, and it is paid per field. Rejecting on len()
// makes that O(1) instead of O(n²).
//
// 2. Parse, rejecting non-numeric and negative input (pre-existing behaviour).
//
// 3. BitLen() <= 256. This is the authoritative range check and the one that
// closes the silent-truncation gap. The length cap alone is NOT sufficient:
// 78 nines is only 78 characters but has BitLen 260, i.e. it fits the cap
// and still overflows uint256.
//
// errMsg is the caller's message for a malformed or negative value, so each call
// site keeps its own wording; the two range failures append a specific reason.
func ValidateUint256String(value string, errMsg string) (*big.Int, error) {
// 1. Cheap reject before the expensive parse.
if len(value) > MaxUint256DecimalLen {
return nil, errors.Wrapf(sdkerrors.ErrInvalidRequest,
"%s: length %d exceeds the maximum of %d characters", errMsg, len(value), MaxUint256DecimalLen)
}

// 2. Parse.
bi, ok := new(big.Int).SetString(value, 10)
if !ok || bi.Sign() < 0 {
return nil, errors.Wrap(sdkerrors.ErrInvalidRequest, errMsg)
}

// 3. Authoritative uint256 range check.
if bi.BitLen() > MaxUint256Bits {
return nil, errors.Wrapf(sdkerrors.ErrInvalidRequest,
"%s: value exceeds the uint256 range", errMsg)
}

return bi, nil
}
Loading
Loading