Security Finding: Contract ABI integer inputs wrap silently instead of rejecting out-of-range values
Subsystem: contract
Severity: Medium
Model: claude-sonnet-4.6, default
Root Cause
contract.Spec.funcArgsToScVals() forwards caller-supplied arguments directly into nativeToScVal() without enforcing the declared integer range. For u64 / i64 / u128 / i128 / u256 / i256 / Timepoint / Duration, the SDK then delegates to XdrLargeInt or xdr.Uint64, which encode fixed-width values after truncating them to the target width instead of rejecting oversized inputs.
The high-level contract client path inherits this behavior unchanged: generated client methods call spec.funcArgsToScVals(method, args) before assembly, so an out-of-range value supplied by the application is silently transformed into a different on-chain argument.
Attack Vector
An attacker who can influence a dapp or backend's contract-call parameters can submit oversized integer values that look materially different from the value eventually signed and sent. For example, 2^64 encodes as 0 for u64 / Timepoint, 2^128 encodes as 0 for u128, and 2^127 encodes as INT_MIN for i128.
This is realistic anywhere the SDK is used as the ABI-validation boundary for untrusted user input, API payloads, or cross-service messages. The issue does not require a malicious RPC server.
PoC Summary
Confirmed with a minimal one-argument contract spec and a Vitest PoC. The test calls spec.funcArgsToScVals() with out-of-range string and bigint inputs, confirms the call does not throw, and then decodes the emitted ScVal back to the wrapped value.
The PoC passed for all tested cases:
u64: 18446744073709551616 / 1n << 64n → 0n
Timepoint: 18446744073709551616 / 1n << 64n → 0n
u128: 340282366920938463463374607431768211456 / 1n << 128n → 0n
i128: 170141183460469231731687303715884105728 / 1n << 127n → -2^127
PoC Code
import { describe, expect, it } from "vitest";
import { scValToBigInt } from "@stellar/stellar-base";
import {
StellarSdk,
type ScSpecTypeDef,
} from "../test-utils/stellar-sdk-import";
const { contract, xdr } = StellarSdk;
function makeSingleArgSpec(methodName: string, typeDef: ScSpecTypeDef) {
const entry = xdr.ScSpecEntry.scSpecEntryFunctionV0(
new xdr.ScSpecFunctionV0({
doc: "",
name: methodName,
inputs: [
new xdr.ScSpecFunctionInputV0({
doc: "",
name: "value",
type: typeDef,
}),
],
outputs: [typeDef],
}),
);
return new contract.Spec([entry]);
}
describe("Security PoC - contract ABI integer wraparound", () => {
it("encodes out-of-range integer inputs as wrapped values instead of rejecting them", () => {
const intMinI128 = -(1n << 127n);
const cases = [
{
label: "u64 string input",
methodName: "wrap_u64_string",
typeDef: xdr.ScSpecTypeDef.scSpecTypeU64(),
input: "18446744073709551616",
expected: 0n,
},
{
label: "u64 bigint input",
methodName: "wrap_u64_bigint",
typeDef: xdr.ScSpecTypeDef.scSpecTypeU64(),
input: 1n << 64n,
expected: 0n,
},
{
label: "timepoint string input",
methodName: "wrap_timepoint_string",
typeDef: xdr.ScSpecTypeDef.scSpecTypeTimepoint(),
input: "18446744073709551616",
expected: 0n,
},
{
label: "timepoint bigint input",
methodName: "wrap_timepoint_bigint",
typeDef: xdr.ScSpecTypeDef.scSpecTypeTimepoint(),
input: 1n << 64n,
expected: 0n,
},
{
label: "u128 string input",
methodName: "wrap_u128_string",
typeDef: xdr.ScSpecTypeDef.scSpecTypeU128(),
input: "340282366920938463463374607431768211456",
expected: 0n,
},
{
label: "u128 bigint input",
methodName: "wrap_u128_bigint",
typeDef: xdr.ScSpecTypeDef.scSpecTypeU128(),
input: 1n << 128n,
expected: 0n,
},
{
label: "i128 string input",
methodName: "wrap_i128_string",
typeDef: xdr.ScSpecTypeDef.scSpecTypeI128(),
input: "170141183460469231731687303715884105728",
expected: intMinI128,
},
{
label: "i128 bigint input",
methodName: "wrap_i128_bigint",
typeDef: xdr.ScSpecTypeDef.scSpecTypeI128(),
input: 1n << 127n,
expected: intMinI128,
},
];
for (const testCase of cases) {
const spec = makeSingleArgSpec(testCase.methodName, testCase.typeDef);
let scVals: ReturnType<typeof spec.funcArgsToScVals> | undefined;
expect(() => {
scVals = spec.funcArgsToScVals(testCase.methodName, {
value: testCase.input,
});
}, testCase.label).not.toThrow();
expect(scVals, testCase.label).toHaveLength(1);
expect(scValToBigInt(scVal), testCase.label).toBe(testCase.expected);
expect(spec.scValToNative<bigint>(scVal, testCase.typeDef)).toBe(
testCase.expected,
);
}
});
});
Recommendation
Add explicit min/max validation in Spec.nativeToScVal() and stringToScVal() for every fixed-width integer-like contract type before delegating to XDR helpers. Reject out-of-range string, number, and bigint inputs with a RangeError, and tighten jsonSchema() so its unsigned/signed integer definitions express the true numeric bounds instead of only digit-count limits.
This issue was generated by an automated AI security analysis pipeline.
Security Finding: Contract ABI integer inputs wrap silently instead of rejecting out-of-range values
Subsystem: contract
Severity: Medium
Model: claude-sonnet-4.6, default
Root Cause
contract.Spec.funcArgsToScVals()forwards caller-supplied arguments directly intonativeToScVal()without enforcing the declared integer range. Foru64/i64/u128/i128/u256/i256/Timepoint/Duration, the SDK then delegates toXdrLargeIntorxdr.Uint64, which encode fixed-width values after truncating them to the target width instead of rejecting oversized inputs.The high-level contract client path inherits this behavior unchanged: generated client methods call
spec.funcArgsToScVals(method, args)before assembly, so an out-of-range value supplied by the application is silently transformed into a different on-chain argument.Attack Vector
An attacker who can influence a dapp or backend's contract-call parameters can submit oversized integer values that look materially different from the value eventually signed and sent. For example,
2^64encodes as0foru64/Timepoint,2^128encodes as0foru128, and2^127encodes asINT_MINfori128.This is realistic anywhere the SDK is used as the ABI-validation boundary for untrusted user input, API payloads, or cross-service messages. The issue does not require a malicious RPC server.
PoC Summary
Confirmed with a minimal one-argument contract spec and a Vitest PoC. The test calls
spec.funcArgsToScVals()with out-of-range string andbigintinputs, confirms the call does not throw, and then decodes the emittedScValback to the wrapped value.The PoC passed for all tested cases:
u64:18446744073709551616/1n << 64n→0nTimepoint:18446744073709551616/1n << 64n→0nu128:340282366920938463463374607431768211456/1n << 128n→0ni128:170141183460469231731687303715884105728/1n << 127n→-2^127PoC Code
Recommendation
Add explicit min/max validation in
Spec.nativeToScVal()andstringToScVal()for every fixed-width integer-like contract type before delegating to XDR helpers. Reject out-of-rangestring,number, andbigintinputs with aRangeError, and tightenjsonSchema()so its unsigned/signed integer definitions express the true numeric bounds instead of only digit-count limits.This issue was generated by an automated AI security analysis pipeline.