diff --git a/serde-generate/src/solidity.rs b/serde-generate/src/solidity.rs index 18211caa3..81a391b66 100644 --- a/serde-generate/src/solidity.rs +++ b/serde-generate/src/solidity.rs @@ -894,6 +894,13 @@ function bcs_serialize_{name}({name} memory input) }; writeln!(out, " {block};")?; } + // Deserialize fields directly into the `result` struct in memory. + // Holding each decoded field as a stack local until the final + // constructor call burns ~2 stack slots per field, which trips + // Yul's 16-slot limit for structs with >7 or so fields. Writing + // into `result.` mstores the value straight into memory + // and immediately frees the slot, keeping the stack flat at + // O(1) regardless of field count. writeln!( out, r#"}} @@ -901,33 +908,22 @@ function bcs_serialize_{name}({name} memory input) function bcs_deserialize_offset_{name}(uint256 pos, bytes memory input) internal pure - returns (uint256, {name} memory) + returns (uint256, {name} memory result) {{ uint256 new_pos;"# )?; for (index, named_format) in formats.iter().enumerate() { - let data_location = sol_registry.data_location(&named_format.value); - let qualified_code_name = sol_registry.qualified_code_name(&named_format.value); let key_name = named_format.value.key_name(); let safe_name = safe_variable(&named_format.name); let start_pos = if index == 0 { "pos" } else { "new_pos" }; let deser_fn = sol_registry.qualified_fn_name("bcs_deserialize_offset", &key_name); - writeln!(out, " {qualified_code_name}{data_location} {safe_name};")?; writeln!( out, - " (new_pos, {safe_name}) = {deser_fn}({start_pos}, input);" + " (new_pos, result.{safe_name}) = {deser_fn}({start_pos}, input);" )?; } - writeln!( - out, - " return (new_pos, {name}({}));", - formats - .iter() - .map(|named_format| safe_variable(&named_format.name)) - .collect::>() - .join(", ") - )?; + writeln!(out, " return (new_pos, result);")?; writeln!(out, "}}")?; output_generic_bcs_deserialize(out, name, name, true)?; } diff --git a/serde-generate/tests/solidity_runtime.rs b/serde-generate/tests/solidity_runtime.rs index b2a0edad2..e3418006a 100644 --- a/serde-generate/tests/solidity_runtime.rs +++ b/serde-generate/tests/solidity_runtime.rs @@ -602,3 +602,129 @@ contract ExampleCode {{ test_contract(bytecode.clone(), fct_args); Ok(()) } + +// Wide struct of varied field types. With the old codegen pattern (one stack +// local per decoded field, plus the tuple-return slot), Yul ran out of the +// 16-slot stack budget when this deserializer is inlined into an external +// entry point. With the direct-into-result codegen the stack stays flat and +// the deserializer round-trips correctly regardless of width. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct WideMixedStruct { + pub a: bool, + pub b: u8, + pub c: u16, + pub d: u32, + pub e: u64, + pub f: u128, + pub g: i8, + pub h: i16, + pub i: i32, + pub j: i64, + pub k: i128, + pub l: String, + #[serde(with = "serde_bytes")] + pub m: Vec, + pub n: Vec, + pub o: Option, + pub p: [u8; 32], + pub q: bool, + pub r: u64, + pub s: u128, + pub t: String, +} + +#[test] +fn test_wide_struct_deserialize_round_trip() -> anyhow::Result<()> { + let registry = get_registry_from_type::(); + let dir = tempdir().unwrap(); + let path = dir.path(); + + let test_library_path = path.join("Library.sol"); + { + let mut test_library_file = File::create(&test_library_path)?; + let name = "Library".to_string(); + let config = CodeGeneratorConfig::new(name); + let generator = solidity::CodeGenerator::new(&config); + generator.output(&mut test_library_file, ®istry).unwrap(); + } + + let test_code_path = path.join("test_code.sol"); + { + let mut test_code_file = File::create(&test_code_path)?; + writeln!( + test_code_file, + r#"/// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "./Library.sol"; + +contract ExampleCode {{ + function test_deserialization(bytes calldata input) external pure {{ + Library.WideMixedStruct memory t = Library.bcs_deserialize_WideMixedStruct(input); + // Spot-check fields across the range to ensure each result. + // assignment landed in the right struct slot. + require(t.a == true, "field a"); + require(t.f == 0x0102030405060708090a0b0c0d0e0f10, "field f"); + require(t.k == -1, "field k"); + require(keccak256(bytes(t.l)) == keccak256(bytes("hello")), "field l"); + require(t.m.length == 4, "field m length"); + require(t.m[0] == 0xaa && t.m[3] == 0xdd, "field m bytes"); + require(t.n.length == 3 && t.n[2] == 99, "field n"); + require(t.o.has_value && t.o.value == 0xfeedbeefdeadc0de, "field o"); + require(t.p[0] == 0x01 && t.p[31] == 0x20, "field p"); + require(bytes(t.t).length == 5, "field t"); + + // Round-trip: serialize back and assert byte-equality with the input. + bytes memory input_rev = Library.bcs_serialize_WideMixedStruct(t); + require(input_rev.length == input.length, "round-trip length"); + for (uint256 idx = 0; idx < input.length; idx++) {{ + require(input[idx] == input_rev[idx], "round-trip byte"); + }} + }} +}} +"# + )?; + } + + let bytecode = get_bytecode(path, "test_code.sol", "ExampleCode")?; + + let t = WideMixedStruct { + a: true, + b: 0x42, + c: 0x4243, + d: 0xdead_beef, + e: 0x0102_0304_0506_0708, + f: 0x0102_0304_0506_0708_090a_0b0c_0d0e_0f10, + g: -1, + h: -2, + i: -3, + j: -4, + k: -1, + l: "hello".to_string(), + m: vec![0xaa, 0xbb, 0xcc, 0xdd], + n: vec![1, 2, 99], + o: Some(0xfeed_beef_dead_c0de), + p: { + let mut a = [0u8; 32]; + for (i, x) in a.iter_mut().enumerate() { + *x = (i + 1) as u8; + } + a + }, + q: false, + r: 0, + s: 0, + t: "world".to_string(), + }; + let expected_input = bcs::to_bytes(&t).unwrap(); + + sol! { + function test_deserialization(bytes calldata input); + } + let input = Bytes::copy_from_slice(&expected_input); + let fct_args = test_deserializationCall { input }; + let fct_args = fct_args.abi_encode().into(); + + test_contract(bytecode, fct_args); + Ok(()) +}