Skip to content

Commit 6a7d564

Browse files
committed
fix(cast): consistent serialization of Uint/Ints depending on actual type
The current implementation dynamically tries to determine if the runtime value can fit in 64 bits, but this leads to inconsistent serialization. For instance if you were decoding an `uint[]`, some of the values that fit in 64 bits will serialize as number while others serialize as string making it require special handling on the user that is consuming the json. This change makes it so it uses the type information to determine the serialization. So the user will always know that specific types will always serialize to a number or a string depending on the number of bits that type uses.
1 parent 77a86e6 commit 6a7d564

File tree

8 files changed

+130
-32
lines changed

8 files changed

+130
-32
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cast/src/args.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -768,7 +768,7 @@ pub async fn run_command(args: CastArgs) -> Result<()> {
768768
let tokens: Vec<serde_json::Value> = tokens
769769
.iter()
770770
.cloned()
771-
.map(|t| serialize_value_as_json(t, None))
771+
.map(|t| serialize_value_as_json(t, None, true))
772772
.collect::<Result<Vec<_>>>()
773773
.unwrap();
774774
let _ = sh_println!("{}", serde_json::to_string_pretty(&tokens).unwrap());

crates/cast/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ impl<P: Provider<AnyNetwork> + Clone + Unpin> Cast<P> {
190190
} else if shell::is_json() {
191191
let tokens = decoded
192192
.into_iter()
193-
.map(|value| serialize_value_as_json(value, None))
193+
.map(|value| serialize_value_as_json(value, None, true))
194194
.collect::<eyre::Result<Vec<_>>>()?;
195195
serde_json::to_string_pretty(&tokens).unwrap()
196196
} else {
@@ -2483,7 +2483,7 @@ mod tests {
24832483
let calldata = "0xdb5b0ed700000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006772bf190000000000000000000000000000000000000000000000000000000000020716000000000000000000000000af9d27ffe4d51ed54ac8eec78f2785d7e11e5ab100000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000000000000000000000000000000000000000000404366a6dc4b2f348a85e0066e46f0cc206fca6512e0ed7f17ca7afb88e9a4c27000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000093922dee6e380c28a50c008ab167b7800bb24c2026cd1b22f1c6fb884ceed7400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060f85e59ecad6c1a6be343a945abedb7d5b5bfad7817c4d8cc668da7d391faf700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000093dfbf04395fbec1f1aed4ad0f9d3ba880ff58a60485df5d33f8f5e0fb73188600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000aa334a426ea9e21d5f84eb2d4723ca56b92382b9260ab2b6769b7c23d437b6b512322a25cecc954127e60cf91ef056ac1da25f90b73be81c3ff1872fa48d10c7ef1ccb4087bbeedb54b1417a24abbb76f6cd57010a65bb03c7b6602b1eaf0e32c67c54168232d4edc0bfa1b815b2af2a2d0a5c109d675a4f2de684e51df9abb324ab1b19a81bac80f9ce3a45095f3df3a7cf69ef18fc08e94ac3cbc1c7effeacca68e3bfe5d81e26a659b5";
24842484
let sig = "sequenceBatchesValidium((bytes32,bytes32,uint64,bytes32)[],uint64,uint64,address,bytes)";
24852485
let decoded = Cast::calldata_decode(sig, calldata, true).unwrap();
2486-
let json_value = serialize_value_as_json(DynSolValue::Array(decoded), None).unwrap();
2486+
let json_value = serialize_value_as_json(DynSolValue::Array(decoded), None, true).unwrap();
24872487
let expected = serde_json::json!([
24882488
[
24892489
[

crates/cast/tests/cli/selectors.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ casttest!(event_decode_with_sig, |_prj, cmd| {
140140

141141
cmd.args(["--json"]).assert_success().stdout_eq(str![[r#"
142142
[
143-
78,
143+
"78",
144144
"0x0000000000000000000000000000000000D0004F"
145145
]
146146
@@ -168,7 +168,7 @@ casttest!(error_decode_with_sig, |_prj, cmd| {
168168

169169
cmd.args(["--json"]).assert_success().stdout_eq(str![[r#"
170170
[
171-
101,
171+
"101",
172172
"0x0000000000000000000000000000000000D0004F"
173173
]
174174

crates/cheatcodes/src/json.rs

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,8 @@ impl Cheatcode for serializeJsonType_0Call {
317317
let Self { typeDescription, value } = self;
318318
let ty = resolve_type(typeDescription, state.struct_defs())?;
319319
let value = ty.abi_decode(value)?;
320-
let value = foundry_common::fmt::serialize_value_as_json(value, state.struct_defs())?;
320+
let value =
321+
foundry_common::fmt::serialize_value_as_json(value, state.struct_defs(), false)?;
321322
Ok(value.to_string().abi_encode())
322323
}
323324
}
@@ -654,7 +655,7 @@ fn serialize_json(
654655
value_key: &str,
655656
value: DynSolValue,
656657
) -> Result {
657-
let value = foundry_common::fmt::serialize_value_as_json(value, state.struct_defs())?;
658+
let value = foundry_common::fmt::serialize_value_as_json(value, state.struct_defs(), false)?;
658659
let map = state.serialized_jsons.entry(object_key.into()).or_default();
659660
map.insert(value_key.into(), value);
660661
let stringified = serde_json::to_string(map).unwrap();
@@ -884,7 +885,7 @@ mod tests {
884885
proptest::proptest! {
885886
#[test]
886887
fn test_json_roundtrip_guessed(v in guessable_types()) {
887-
let json = serialize_value_as_json(v.clone(), None).unwrap();
888+
let json = serialize_value_as_json(v.clone(), None, false).unwrap();
888889
let value = json_value_to_token(&json, None).unwrap();
889890

890891
// do additional abi_encode -> abi_decode to avoid zero signed integers getting decoded as unsigned and causing assert_eq to fail.
@@ -894,14 +895,14 @@ mod tests {
894895

895896
#[test]
896897
fn test_json_roundtrip(v in any::<DynSolValue>().prop_filter("filter out values without type", |v| v.as_type().is_some())) {
897-
let json = serialize_value_as_json(v.clone(), None).unwrap();
898+
let json = serialize_value_as_json(v.clone(), None, false).unwrap();
898899
let value = parse_json_as(&json, &v.as_type().unwrap()).unwrap();
899900
assert_eq!(value, v);
900901
}
901902

902903
#[test]
903904
fn test_json_roundtrip_with_struct_defs((struct_defs, v) in custom_struct_strategy()) {
904-
let json = serialize_value_as_json(v.clone(), Some(&struct_defs)).unwrap();
905+
let json = serialize_value_as_json(v.clone(), Some(&struct_defs), false).unwrap();
905906
let sol_type = v.as_type().unwrap();
906907
let parsed_value = parse_json_as(&json, &sol_type).unwrap();
907908
assert_eq!(parsed_value, v);
@@ -1060,7 +1061,8 @@ mod tests {
10601061
};
10611062

10621063
// Serialize the value to JSON and verify that the order is preserved.
1063-
let json_value = serialize_value_as_json(item_struct, Some(&struct_defs.into())).unwrap();
1064+
let json_value =
1065+
serialize_value_as_json(item_struct, Some(&struct_defs.into()), false).unwrap();
10641066
let json_string = serde_json::to_string(&json_value).unwrap();
10651067
assert_eq!(json_string, r#"{"name":"Test Item","id":123,"active":true}"#);
10661068
}
@@ -1092,9 +1094,12 @@ mod tests {
10921094
};
10931095

10941096
// Serialize it. The resulting JSON should respect the struct definition order.
1095-
let json_value =
1096-
serialize_value_as_json(original_wallet.clone(), Some(&struct_defs.clone().into()))
1097-
.unwrap();
1097+
let json_value = serialize_value_as_json(
1098+
original_wallet.clone(),
1099+
Some(&struct_defs.clone().into()),
1100+
false,
1101+
)
1102+
.unwrap();
10981103
let json_string = serde_json::to_string(&json_value).unwrap();
10991104
assert_eq!(
11001105
json_string,

crates/common/fmt/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,4 @@ yansi.workspace = true
3232
[dev-dependencies]
3333
foundry-macros.workspace = true
3434
similar-asserts.workspace = true
35+
proptest.workspace = true
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Seeds for failure cases proptest has generated in the past. It is
2+
# automatically read and these particular cases re-run before any
3+
# novel cases are generated.
4+
#
5+
# It is recommended to check this file in to source control so that
6+
# everyone who runs the test benefits from these saved cases.
7+
cc 885aa25152cd93b8ddf5e98d7bfdc995d70d059b823b5589e793df41be92d9ce # shrinks to l = 0, h = 18446744073709551616

crates/common/fmt/src/dynamic.rs

Lines changed: 102 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -153,15 +153,20 @@ pub fn format_token_raw(value: &DynSolValue) -> String {
153153
pub fn serialize_value_as_json(
154154
value: DynSolValue,
155155
defs: Option<&StructDefinitions>,
156+
strict: bool,
156157
) -> Result<Value> {
157158
if let Some(defs) = defs {
158-
_serialize_value_as_json(value, defs)
159+
_serialize_value_as_json(value, defs, strict)
159160
} else {
160-
_serialize_value_as_json(value, &StructDefinitions::default())
161+
_serialize_value_as_json(value, &StructDefinitions::default(), strict)
161162
}
162163
}
163164

164-
fn _serialize_value_as_json(value: DynSolValue, defs: &StructDefinitions) -> Result<Value> {
165+
fn _serialize_value_as_json(
166+
value: DynSolValue,
167+
defs: &StructDefinitions,
168+
strict: bool,
169+
) -> Result<Value> {
165170
match value {
166171
DynSolValue::Bool(b) => Ok(Value::Bool(b)),
167172
DynSolValue::String(s) => {
@@ -175,34 +180,38 @@ fn _serialize_value_as_json(value: DynSolValue, defs: &StructDefinitions) -> Res
175180
}
176181
DynSolValue::Bytes(b) => Ok(Value::String(hex::encode_prefixed(b))),
177182
DynSolValue::FixedBytes(b, size) => Ok(Value::String(hex::encode_prefixed(&b[..size]))),
178-
DynSolValue::Int(i, _) => {
179-
if let Ok(n) = i64::try_from(i) {
180-
// Use `serde_json::Number` if the number can be accurately represented.
181-
Ok(Value::Number(n.into()))
182-
} else {
183+
DynSolValue::Int(i, bits) => {
184+
match (i64::try_from(i), strict) {
185+
// In strict mode, return as number only if the type dictates so
186+
(Ok(n), true) if bits <= 64 => Ok(Value::Number(n.into())),
187+
// In normal mode, return as number if the number can be accurately represented.
188+
(Ok(n), false) => Ok(Value::Number(n.into())),
183189
// Otherwise, fallback to its string representation to preserve precision and ensure
184190
// compatibility with alloy's `DynSolType` coercion.
185-
Ok(Value::String(i.to_string()))
191+
_ => Ok(Value::String(i.to_string())),
186192
}
187193
}
188-
DynSolValue::Uint(i, _) => {
189-
if let Ok(n) = u64::try_from(i) {
190-
// Use `serde_json::Number` if the number can be accurately represented.
191-
Ok(Value::Number(n.into()))
192-
} else {
194+
DynSolValue::Uint(i, bits) => {
195+
match (u64::try_from(i), strict) {
196+
// In strict mode, return as number only if the type dictates so
197+
(Ok(n), true) if bits <= 64 => Ok(Value::Number(n.into())),
198+
// In normal mode, return as number if the number can be accurately represented.
199+
(Ok(n), false) => Ok(Value::Number(n.into())),
193200
// Otherwise, fallback to its string representation to preserve precision and ensure
194201
// compatibility with alloy's `DynSolType` coercion.
195-
Ok(Value::String(i.to_string()))
202+
_ => Ok(Value::String(i.to_string())),
196203
}
197204
}
198205
DynSolValue::Address(a) => Ok(Value::String(a.to_string())),
199206
DynSolValue::Array(e) | DynSolValue::FixedArray(e) => Ok(Value::Array(
200-
e.into_iter().map(|v| _serialize_value_as_json(v, defs)).collect::<Result<_>>()?,
207+
e.into_iter()
208+
.map(|v| _serialize_value_as_json(v, defs, strict))
209+
.collect::<Result<_>>()?,
201210
)),
202211
DynSolValue::CustomStruct { name, prop_names, tuple } => {
203212
let values = tuple
204213
.into_iter()
205-
.map(|v| _serialize_value_as_json(v, defs))
214+
.map(|v| _serialize_value_as_json(v, defs, strict))
206215
.collect::<Result<Vec<_>>>()?;
207216
let mut map: HashMap<String, Value> = prop_names.into_iter().zip(values).collect();
208217

@@ -222,7 +231,10 @@ fn _serialize_value_as_json(value: DynSolValue, defs: &StructDefinitions) -> Res
222231
Ok(Value::Object(map.into_iter().collect::<Map<String, Value>>()))
223232
}
224233
DynSolValue::Tuple(values) => Ok(Value::Array(
225-
values.into_iter().map(|v| _serialize_value_as_json(v, defs)).collect::<Result<_>>()?,
234+
values
235+
.into_iter()
236+
.map(|v| _serialize_value_as_json(v, defs, strict))
237+
.collect::<Result<_>>()?,
226238
)),
227239
DynSolValue::Function(_) => eyre::bail!("cannot serialize function pointer"),
228240
}
@@ -318,4 +330,76 @@ mod tests {
318330
"0xFb6916095cA1Df60bb79ce92cE3EA74c37c5d359"
319331
);
320332
}
333+
334+
proptest::proptest! {
335+
#[test]
336+
fn test_serialize_uint_as_json(l in 0u64..u64::MAX, h in ((u64::MAX as u128) + 1)..u128::MAX) {
337+
let l_min_bits = (64 - l.leading_zeros()) as usize;
338+
let h_min_bits = (128 - h.leading_zeros()) as usize;
339+
340+
// values that fit in u64 should be serialized as a number in !strict mode
341+
assert_eq!(
342+
serialize_value_as_json(DynSolValue::Uint(l.try_into().unwrap(), l_min_bits), None, false).unwrap(),
343+
serde_json::json!(l)
344+
);
345+
// values that dont fit in u64 should be serialized as a string in !strict mode
346+
assert_eq!(
347+
serialize_value_as_json(DynSolValue::Uint(h.try_into().unwrap(), h_min_bits), None, false).unwrap(),
348+
serde_json::json!(h.to_string())
349+
);
350+
351+
// values should be serialized according to the type
352+
// since l_min_bits <= 64, expect the serialization to be a number
353+
assert_eq!(
354+
serialize_value_as_json(DynSolValue::Uint(l.try_into().unwrap(), l_min_bits), None, true).unwrap(),
355+
serde_json::json!(l)
356+
);
357+
// since `h_min_bits` is specified for the number `l`, expect the serialization to be a string
358+
// even though `l` fits in a u64
359+
assert_eq!(
360+
serialize_value_as_json(DynSolValue::Uint(l.try_into().unwrap(), h_min_bits), None, true).unwrap(),
361+
serde_json::json!(l.to_string())
362+
);
363+
// since `h_min_bits` is specified for the number `h`, expect the serialization to be a string
364+
assert_eq!(
365+
serialize_value_as_json(DynSolValue::Uint(h.try_into().unwrap(), h_min_bits), None, true).unwrap(),
366+
serde_json::json!(h.to_string())
367+
);
368+
}
369+
370+
#[test]
371+
fn test_serialize_int_as_json(l in 0i64..=i64::MAX, h in ((i64::MAX as i128) + 1)..=i128::MAX) {
372+
let l_min_bits = (64 - (l as u64).leading_zeros()) as usize + 1;
373+
let h_min_bits = (128 - (h as u128).leading_zeros()) as usize + 1;
374+
375+
// values that fit in i64 should be serialized as a number in !strict mode
376+
assert_eq!(
377+
serialize_value_as_json(DynSolValue::Int(l.try_into().unwrap(), l_min_bits), None, false).unwrap(),
378+
serde_json::json!(l)
379+
);
380+
// values that dont fit in i64 should be serialized as a string in !strict mode
381+
assert_eq!(
382+
serialize_value_as_json(DynSolValue::Int(h.try_into().unwrap(), h_min_bits), None, false).unwrap(),
383+
serde_json::json!(h.to_string())
384+
);
385+
386+
// values should be serialized according to the type
387+
// since l_min_bits <= 64, expect the serialization to be a number
388+
assert_eq!(
389+
serialize_value_as_json(DynSolValue::Int(l.try_into().unwrap(), l_min_bits), None, true).unwrap(),
390+
serde_json::json!(l)
391+
);
392+
// since `h_min_bits` is specified for the number `l`, expect the serialization to be a string
393+
// even though `l` fits in an i64
394+
assert_eq!(
395+
serialize_value_as_json(DynSolValue::Int(l.try_into().unwrap(), h_min_bits), None, true).unwrap(),
396+
serde_json::json!(l.to_string())
397+
);
398+
// since `h_min_bits` is specified for the number `h`, expect the serialization to be a string
399+
assert_eq!(
400+
serialize_value_as_json(DynSolValue::Int(h.try_into().unwrap(), h_min_bits), None, true).unwrap(),
401+
serde_json::json!(h.to_string())
402+
);
403+
}
404+
}
321405
}

0 commit comments

Comments
 (0)