Skip to content
Draft
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
10 changes: 10 additions & 0 deletions gnd/src/abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ pub fn resolve_event_param_type(param: &EventParam) -> DynSolType {
.expect("valid ABI type")
}

/// Parse a selector type without panicking.
///
/// [`resolve_event_param_type`] `.expect()`s because codegen only runs on ABIs
/// that already resolved. Scaffolding also runs on hand-written ABIs that alloy
/// accepts but cannot fully resolve (a component-less `tuple`), so it must fall
/// back to a `Bytes` field rather than abort. Returns `None` for such a type.
pub fn try_resolve_selector_type(selector_type: &str) -> Option<DynSolType> {
selector_type.parse::<DynSolType>().ok()
}

/// The type an indexed param actually carries in the log.
///
/// A topic is exactly 32 bytes, so reference types (strings, bytes, arrays and
Expand Down
51 changes: 42 additions & 9 deletions gnd/src/scaffold/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use std::collections::HashMap;

use graph::abi::{Event, EventParam, Param};
use graph::abi::{DynSolType, Event, EventParam, Param};

use super::ScaffoldOptions;
use crate::shared::handle_reserved_word;
Expand Down Expand Up @@ -179,12 +179,12 @@ pub fn event_param_accessors(inputs: &[EventParam]) -> Vec<String> {
}

/// A flattened leaf of an event input: the entity field name, the matching
/// `event.params` accessor, and the leaf's Solidity type. A single `tuple`
/// `event.params` accessor, and the leaf's resolved type. A single `tuple`
/// expands to one leaf per component (`data` -> `data_a`, `data_b`).
pub struct InputLeaf {
pub field: String,
pub accessor: String,
pub solidity_type: String,
pub ty: DynSolType,
}

/// Flatten an event's inputs into leaves, unrolling a single `tuple` into its
Expand All @@ -203,15 +203,15 @@ pub fn flatten_event_inputs(inputs: &[EventParam]) -> Vec<InputLeaf> {
leaves.push(InputLeaf {
field: super::sanitize_field_name(&input.name),
accessor: accessor.clone(),
solidity_type: "bytes32".to_string(),
ty: DynSolType::FixedBytes(32),
});
continue;
}
flatten_into(
&mut leaves,
std::slice::from_ref(accessor),
&[super::sanitize_field_name(&input.name)],
&input.ty,
&input.selector_type(),
&input.components,
);
}
Expand All @@ -238,14 +238,24 @@ fn flatten_into(
out: &mut Vec<InputLeaf>,
accessor_path: &[String],
field_path: &[String],
solidity_type: &str,
selector_type: &str,
components: &[Param],
) {
if solidity_type != "tuple" {
// `Tuple(_)` is the unroll discriminator; its payload is never read (names and
// recursion come from `components`), so dropping the component names alloy does
// is harmless. `tuple[]` resolves to `Array(Tuple(_))` and stays a leaf.
let resolved = crate::abi::try_resolve_selector_type(selector_type);
if !matches!(resolved, Some(DynSolType::Tuple(_))) {
let ty = resolved.unwrap_or_else(|| {
// alloy accepted the ABI but can't resolve this type (a component-less
// `tuple`); emit a Bytes leaf rather than abort scaffolding.
eprintln!("warning: scaffold could not resolve type `{selector_type}`, using Bytes");
DynSolType::Bytes
});
out.push(InputLeaf {
field: field_path.join("_"),
accessor: accessor_path.join("."),
solidity_type: solidity_type.to_string(),
ty,
});
return;
}
Expand All @@ -260,7 +270,13 @@ fn flatten_into(
accessor.push(raw);
let mut fields = field_path.to_vec();
fields.push(field);
flatten_into(out, &accessor, &fields, &comp.ty, &comp.components);
flatten_into(
out,
&accessor,
&fields,
&comp.selector_type(),
&comp.components,
);
}
}

Expand Down Expand Up @@ -607,4 +623,21 @@ mod tests {
let fields: Vec<&str> = leaves.iter().map(|l| l.field.as_str()).collect();
assert_eq!(fields, vec!["value", "value2", "value1"]);
}

#[test]
fn test_flatten_component_less_tuple_falls_back_to_bytes() {
// alloy accepts a `tuple` with no components (solc never emits one), whose
// selector type is the bare "tuple" and does not resolve. Scaffolding must
// fall back to a single Bytes leaf, not panic like codegen's resolver.
let input = EventParam {
name: "data".to_string(),
ty: "tuple".to_string(),
components: vec![],
..Default::default()
};
let leaves = flatten_event_inputs(&[input]);
assert_eq!(leaves.len(), 1);
assert_eq!(leaves[0].field, "data");
assert_eq!(leaves[0].ty, DynSolType::Bytes);
}
}
70 changes: 65 additions & 5 deletions gnd/src/scaffold/mapping.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! Mapping (AssemblyScript) generation for scaffold.

use graph::abi::DynSolType;

use super::ScaffoldOptions;
use super::manifest::{EventInfo, ResolvedEvent, extract_events_from_abi};
use super::sanitize_field_name;
Expand Down Expand Up @@ -205,7 +207,7 @@ fn generate_single_handler(resolved: &ResolvedEvent) -> String {
// param<index> for unnamed).
let mut field_assignments = String::new();
for leaf in super::flatten_event_inputs(&resolved.event.inputs) {
if needs_bytes_array_cast(&leaf.solidity_type) {
if needs_bytes_array_cast(&leaf.ty) {
field_assignments.push_str(&format!(
" entity.{} = changetype<Bytes[]>(event.params.{})\n",
leaf.field, leaf.accessor
Expand Down Expand Up @@ -234,16 +236,25 @@ fn generate_single_handler(resolved: &ResolvedEvent) -> String {
)
}

/// Whether a leaf is an `address[]`, whose binding type `Array<Address>` needs a
/// `changetype` to fit a `Bytes[]` entity field.
/// Whether a leaf is an address array (`address[]` or `address[N]`), whose
/// binding type `Array<Address>` needs a `changetype` to fit a `Bytes[]` entity
/// field.
///
/// Only `address` qualifies: `Address extends Bytes`, so the cast is an upcast.
/// `ethereum.Tuple` extends `Array<Value>` and is not a `Bytes`, so casting a
/// `tuple[]` would be an unchecked reinterpret that compiles and then writes
/// heap pointers into the entity. It has no `Bytes[]` form, so it gets no cast
/// and fails to compile instead.
fn needs_bytes_array_cast(solidity_type: &str) -> bool {
solidity_type == "address[]"
///
/// An indexed address array reaches the entity as a `bytes32` hash leaf, not an
/// array (`flatten_event_inputs` short-circuits it), so it never gets here and
/// this cast cannot reinterpret a hash.
fn needs_bytes_array_cast(ty: &DynSolType) -> bool {
matches!(
ty,
DynSolType::Array(inner) | DynSolType::FixedArray(inner, _)
if matches!(**inner, DynSolType::Address)
)
}

/// Extract callable functions from ABI for documentation comments.
Expand Down Expand Up @@ -522,6 +533,55 @@ mod tests {
);
}

#[test]
fn test_generate_mapping_casts_fixed_size_address_array() {
// A non-indexed `address[N]` reaches the entity as an array, so it needs
// the same upcast as `address[]`; a fixed uint array does not.
let abi = json!([
{
"type": "event",
"name": "Signers",
"inputs": [
{"name": "owners", "type": "address[3]"},
{"name": "amounts", "type": "uint256[3]"}
]
}
]);

let options = ScaffoldOptions {
contract_name: "Vault".to_string(),
abi: Some(abi),
index_events: true,
..Default::default()
};

let mapping = generate_mapping(&options);
assert!(
mapping.contains("entity.owners = changetype<Bytes[]>(event.params.owners)"),
"{}",
mapping
);
assert!(
mapping.contains("entity.amounts = event.params.amounts\n"),
"{}",
mapping
);
}

#[test]
fn test_needs_bytes_array_cast() {
let cast = |t: &str| needs_bytes_array_cast(&t.parse::<DynSolType>().unwrap());
// Both dynamic and fixed-size address arrays need the upcast to Bytes[].
assert!(cast("address[]"));
assert!(cast("address[3]"));
// Nothing else does: a plain address, a non-address array, or a tuple
// array (which has no Bytes[] form and must fail to compile instead).
assert!(!cast("address"));
assert!(!cast("uint256[]"));
assert!(!cast("uint256[3]"));
assert!(!cast("(address,uint256)[]"));
}

#[test]
fn test_generate_mapping_indexed_reference_params_are_hashes() {
// Indexed reference types are keccak'd into a topic, so the binding
Expand Down
Loading