Skip to content

Commit 12da30f

Browse files
committed
Enforce NEP-25 extended type validation
NEP-25 adds ExtendedType descriptors to contract manifests, but the branch currently accepts manifests that reference undefined named types, mismatch parameter metadata, or break the base NEP-25 rules. This commit adds ExtendedType validation to ContractAbi, ContractMethodDescriptor, and ContractParameterDefinition so contracts that violate NEP-25 are rejected before deployment.\n\nThe change keeps the detailed FormatException messages recently introduced in dev, ensuring operators get actionable errors while we enforce the extended-type invariants.
1 parent 7cd74e6 commit 12da30f

File tree

6 files changed

+317
-10
lines changed

6 files changed

+317
-10
lines changed

src/Neo/SmartContract/Manifest/ContractAbi.cs

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ void IInteroperable.FromStackItem(StackItem stackItem)
5454
NamedTypes = ((Map)data[2]).ToDictionary(p => p.Key.GetString()!, p => p.Value.ToInteroperable<ExtendedType>());
5555
else
5656
NamedTypes = null;
57+
58+
ValidateExtendedTypes();
5759
}
5860

5961
public StackItem ToStackItem(IReferenceCounter referenceCounter)
@@ -75,16 +77,70 @@ public StackItem ToStackItem(IReferenceCounter referenceCounter)
7577
/// <returns>The converted ABI.</returns>
7678
public static ContractAbi FromJson(JObject json)
7779
{
80+
Dictionary<string, ExtendedType>? namedTypes = null;
81+
var knownNamedTypes = new HashSet<string>(StringComparer.Ordinal);
82+
if (json!["namedtypes"] is JObject namedTypesJson)
83+
{
84+
foreach (var key in namedTypesJson.Properties.Keys)
85+
{
86+
knownNamedTypes.Add(key);
87+
}
88+
89+
namedTypes = new Dictionary<string, ExtendedType>(namedTypesJson.Properties.Count, StringComparer.Ordinal);
90+
foreach (var (name, token) in namedTypesJson.Properties)
91+
{
92+
if (token is not JObject valueObject)
93+
throw new FormatException("Named type definition must be a JSON object.");
94+
namedTypes[name] = ExtendedType.FromJson(valueObject);
95+
}
96+
}
97+
7898
ContractAbi abi = new()
7999
{
80-
Methods = ((JArray)json!["methods"]!)?.Select(u => ContractMethodDescriptor.FromJson((JObject)u!)).ToArray() ?? [],
81-
Events = ((JArray)json!["events"]!)?.Select(u => ContractEventDescriptor.FromJson((JObject)u!)).ToArray() ?? [],
82-
NamedTypes = ((JObject)json!["namedtypes"]!)?.Properties.ToDictionary(u => u.Key, u => ExtendedType.FromJson((JObject)u.Value!))
100+
Methods = ((JArray)json!["methods"]!)?.Select(u => ContractMethodDescriptor.FromJson((JObject)u!, knownNamedTypes)).ToArray() ?? [],
101+
Events = ((JArray)json!["events"]!)?.Select(u => ContractEventDescriptor.FromJson((JObject)u!, knownNamedTypes)).ToArray() ?? [],
102+
NamedTypes = namedTypes
83103
};
84104
if (abi.Methods.Length == 0) throw new FormatException("Methods in ContractAbi is empty");
105+
106+
abi.ValidateExtendedTypes();
85107
return abi;
86108
}
87109

110+
internal void ValidateExtendedTypes()
111+
{
112+
ISet<string> knownNamedTypes = NamedTypes != null
113+
? new HashSet<string>(NamedTypes.Keys, StringComparer.Ordinal)
114+
: new HashSet<string>(StringComparer.Ordinal);
115+
116+
if (NamedTypes != null)
117+
{
118+
foreach (var (name, type) in NamedTypes)
119+
{
120+
ExtendedType.EnsureValidNamedTypeIdentifier(name);
121+
type.ValidateForNamedTypeDefinition(knownNamedTypes);
122+
}
123+
}
124+
125+
foreach (var method in Methods)
126+
{
127+
foreach (var parameter in method.Parameters)
128+
{
129+
parameter.ExtendedType?.ValidateForParameterOrReturn(parameter.Type, knownNamedTypes);
130+
}
131+
132+
method.ExtendedReturnType?.ValidateForParameterOrReturn(method.ReturnType, knownNamedTypes);
133+
}
134+
135+
foreach (var ev in Events)
136+
{
137+
foreach (var parameter in ev.Parameters)
138+
{
139+
parameter.ExtendedType?.ValidateForParameterOrReturn(parameter.Type, knownNamedTypes);
140+
}
141+
}
142+
}
143+
88144
/// <summary>
89145
/// Gets the method with the specified name.
90146
/// </summary>

src/Neo/SmartContract/Manifest/ContractEventDescriptor.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Neo.VM;
1414
using Neo.VM.Types;
1515
using System;
16+
using System.Collections.Generic;
1617
using System.Linq;
1718
using System.Runtime.CompilerServices;
1819
using Array = Neo.VM.Types.Array;
@@ -54,13 +55,14 @@ public virtual StackItem ToStackItem(IReferenceCounter referenceCounter)
5455
/// Converts the event from a JSON object.
5556
/// </summary>
5657
/// <param name="json">The event represented by a JSON object.</param>
58+
/// <param name="knownNamedTypes">Set of named type identifiers declared in the manifest, if any.</param>
5759
/// <returns>The converted event.</returns>
58-
public static ContractEventDescriptor FromJson(JObject json)
60+
public static ContractEventDescriptor FromJson(JObject json, ISet<string> knownNamedTypes = null)
5961
{
6062
ContractEventDescriptor descriptor = new()
6163
{
6264
Name = json["name"].GetString(),
63-
Parameters = ((JArray)json["parameters"]).Select(u => ContractParameterDefinition.FromJson((JObject)u)).ToArray(),
65+
Parameters = ((JArray)json["parameters"]).Select(u => ContractParameterDefinition.FromJson((JObject)u, knownNamedTypes)).ToArray(),
6466
};
6567
if (string.IsNullOrEmpty(descriptor.Name)) throw new FormatException("Name in ContractEventDescriptor is empty");
6668
_ = descriptor.Parameters.ToDictionary(p => p.Name);

src/Neo/SmartContract/Manifest/ContractMethodDescriptor.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Neo.VM;
1414
using Neo.VM.Types;
1515
using System;
16+
using System.Collections.Generic;
1617
using System.Linq;
1718
using System.Runtime.CompilerServices;
1819

@@ -56,6 +57,7 @@ public override void FromStackItem(StackItem stackItem)
5657
{
5758
ExtendedReturnType = new ExtendedType();
5859
ExtendedReturnType.FromStackItem((VM.Types.Array)item[5]);
60+
ExtendedReturnType.ValidateForParameterOrReturn(ReturnType, null);
5961
}
6062
else
6163
{
@@ -80,13 +82,14 @@ public override StackItem ToStackItem(IReferenceCounter referenceCounter)
8082
/// Converts the method from a JSON object.
8183
/// </summary>
8284
/// <param name="json">The method represented by a JSON object.</param>
85+
/// <param name="knownNamedTypes">Set of named type identifiers declared in the manifest, if any.</param>
8386
/// <returns>The converted method.</returns>
84-
public new static ContractMethodDescriptor FromJson(JObject json)
87+
public new static ContractMethodDescriptor FromJson(JObject json, ISet<string> knownNamedTypes = null)
8588
{
8689
ContractMethodDescriptor descriptor = new()
8790
{
8891
Name = json["name"].GetString(),
89-
Parameters = ((JArray)json["parameters"]).Select(u => ContractParameterDefinition.FromJson((JObject)u)).ToArray(),
92+
Parameters = ((JArray)json["parameters"]).Select(u => ContractParameterDefinition.FromJson((JObject)u, knownNamedTypes)).ToArray(),
9093
ReturnType = Enum.Parse<ContractParameterType>(json["returntype"].GetString()),
9194
Offset = json["offset"].GetInt32(),
9295
Safe = json["safe"].GetBoolean(),
@@ -97,11 +100,11 @@ public override StackItem ToStackItem(IReferenceCounter referenceCounter)
97100
throw new FormatException("Name in ContractMethodDescriptor is empty");
98101

99102
_ = descriptor.Parameters.ToDictionary(p => p.Name);
100-
101103
if (!Enum.IsDefined(typeof(ContractParameterType), descriptor.ReturnType))
102104
throw new FormatException($"ReturnType({descriptor.ReturnType}) in ContractMethodDescriptor is not valid");
103105
if (descriptor.Offset < 0)
104106
throw new FormatException($"Offset({descriptor.Offset}) in ContractMethodDescriptor is not valid");
107+
descriptor.ExtendedReturnType?.ValidateForParameterOrReturn(descriptor.ReturnType, knownNamedTypes);
105108
return descriptor;
106109
}
107110

src/Neo/SmartContract/Manifest/ContractParameterDefinition.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Neo.VM;
1414
using Neo.VM.Types;
1515
using System;
16+
using System.Collections.Generic;
1617
using System.Runtime.CompilerServices;
1718

1819
namespace Neo.SmartContract.Manifest
@@ -47,6 +48,7 @@ public void FromStackItem(StackItem stackItem)
4748
{
4849
ExtendedType = new ExtendedType();
4950
ExtendedType.FromStackItem((VM.Types.Array)item[2]);
51+
ExtendedType.ValidateForParameterOrReturn(Type, null);
5052
}
5153
else
5254
{
@@ -70,8 +72,9 @@ public StackItem ToStackItem(IReferenceCounter referenceCounter)
7072
/// Converts the parameter from a JSON object.
7173
/// </summary>
7274
/// <param name="json">The parameter represented by a JSON object.</param>
75+
/// <param name="knownNamedTypes">Set of named type identifiers declared in the manifest, if any.</param>
7376
/// <returns>The converted parameter.</returns>
74-
public static ContractParameterDefinition FromJson(JObject json)
77+
public static ContractParameterDefinition FromJson(JObject json, ISet<string> knownNamedTypes = null)
7578
{
7679
ContractParameterDefinition parameter = new()
7780
{
@@ -83,6 +86,7 @@ public static ContractParameterDefinition FromJson(JObject json)
8386
throw new FormatException("Name in ContractParameterDefinition is empty");
8487
if (!Enum.IsDefined(typeof(ContractParameterType), parameter.Type) || parameter.Type == ContractParameterType.Void)
8588
throw new FormatException($"Type({parameter.Type}) in ContractParameterDefinition is not valid");
89+
parameter.ExtendedType?.ValidateForParameterOrReturn(parameter.Type, knownNamedTypes);
8690
return parameter;
8791
}
8892

src/Neo/SmartContract/Manifest/ExtendedType.cs

Lines changed: 164 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,62 @@
1313
using Neo.VM;
1414
using Neo.VM.Types;
1515
using System;
16+
using System.Collections.Generic;
17+
using System.Linq;
18+
using System.Text.RegularExpressions;
1619

1720
namespace Neo.SmartContract.Manifest
1821
{
1922
#nullable enable
2023

2124
public class ExtendedType : IInteroperable, IEquatable<ExtendedType>
2225
{
26+
private static readonly Regex NamedTypePattern = new("^[A-Za-z][A-Za-z0-9.]{0,63}$", RegexOptions.Compiled);
27+
28+
private static readonly HashSet<ContractParameterType> LengthAllowedTypes = new()
29+
{
30+
ContractParameterType.Integer,
31+
ContractParameterType.ByteArray,
32+
ContractParameterType.String,
33+
ContractParameterType.Array
34+
};
35+
36+
private static readonly HashSet<ContractParameterType> ForbidNullAllowedTypes = new()
37+
{
38+
ContractParameterType.Hash160,
39+
ContractParameterType.Hash256,
40+
ContractParameterType.ByteArray,
41+
ContractParameterType.String,
42+
ContractParameterType.Array,
43+
ContractParameterType.Map,
44+
ContractParameterType.InteropInterface
45+
};
46+
47+
private static readonly HashSet<ContractParameterType> MapKeyAllowedTypes = new()
48+
{
49+
ContractParameterType.Signature,
50+
ContractParameterType.Boolean,
51+
ContractParameterType.Integer,
52+
ContractParameterType.Hash160,
53+
ContractParameterType.Hash256,
54+
ContractParameterType.ByteArray,
55+
ContractParameterType.PublicKey,
56+
ContractParameterType.String
57+
};
58+
59+
private static FormatException Nep25Error(string message) => new($"Invalid NEP-25 extended type: {message}");
60+
61+
internal static bool IsValidNamedTypeIdentifier(string name)
62+
{
63+
return !string.IsNullOrEmpty(name) && NamedTypePattern.IsMatch(name);
64+
}
65+
66+
internal static void EnsureValidNamedTypeIdentifier(string name)
67+
{
68+
if (!IsValidNamedTypeIdentifier(name))
69+
throw Nep25Error($"Named type '{name}' must start with a letter, contain only alphanumeric characters or dots, and be at most 64 characters long.");
70+
}
71+
2372
/// <summary>
2473
/// The type of the parameter. It can be any value of <see cref="ContractParameterType"/> except <see cref="ContractParameterType.Void"/>.
2574
/// </summary>
@@ -214,6 +263,7 @@ public static ExtendedType FromJson(JObject json)
214263
NamedType = json["namedtype"]?.GetString(),
215264
};
216265
if (!Enum.IsDefined(typeof(ContractParameterType), type.Type)) throw new FormatException();
266+
if (type.Type == ContractParameterType.Void) throw Nep25Error("Void type is not allowed.");
217267
if (json["length"] != null)
218268
{
219269
type.Length = json["length"]!.GetInt32();
@@ -329,7 +379,120 @@ public bool Equals(ExtendedType? other)
329379

330380
return true;
331381
}
382+
383+
internal void ValidateForParameterOrReturn(ContractParameterType expectedType, ISet<string>? knownNamedTypes)
384+
{
385+
ValidateCore(expectedType, allowFields: false, knownNamedTypes, allowNamedTypeReference: true);
386+
}
387+
388+
internal void ValidateForNamedTypeDefinition(ISet<string>? knownNamedTypes)
389+
{
390+
ValidateCore(expectedType: null, allowFields: true, knownNamedTypes, allowNamedTypeReference: true);
391+
}
392+
393+
private void ValidateCore(ContractParameterType? expectedType, bool allowFields, ISet<string>? knownNamedTypes, bool allowNamedTypeReference)
394+
{
395+
if (expectedType.HasValue && Type != expectedType.Value)
396+
throw Nep25Error($"Type mismatch. Expected '{expectedType.Value}', got '{Type}'.");
397+
398+
if (!Enum.IsDefined(typeof(ContractParameterType), Type) || Type == ContractParameterType.Void)
399+
throw Nep25Error($"Unsupported type '{Type}'.");
400+
401+
if (Length.HasValue && !LengthAllowedTypes.Contains(Type))
402+
throw Nep25Error($"length cannot be specified for type '{Type}'.");
403+
404+
if (ForbidNull.HasValue && !ForbidNullAllowedTypes.Contains(Type))
405+
throw Nep25Error($"forbidnull cannot be specified for type '{Type}'.");
406+
407+
if (Interface.HasValue && Type != ContractParameterType.InteropInterface)
408+
throw Nep25Error($"interface can only be used with InteropInterface type.");
409+
410+
if (Type == ContractParameterType.InteropInterface && !Interface.HasValue)
411+
throw Nep25Error("interface is required for InteropInterface type.");
412+
413+
if (Key.HasValue && Type != ContractParameterType.Map)
414+
throw Nep25Error($"key cannot be used with type '{Type}'.");
415+
416+
if (Key.HasValue && !MapKeyAllowedTypes.Contains(Key.Value))
417+
throw Nep25Error($"key '{Key.Value}' is not allowed for map definitions.");
418+
419+
if (Type == ContractParameterType.Map && !Key.HasValue)
420+
throw Nep25Error("key is required for Map type.");
421+
422+
if (NamedType != null)
423+
{
424+
if (!allowNamedTypeReference)
425+
throw Nep25Error("namedtype is not allowed in this context.");
426+
427+
if (Type != ContractParameterType.Array)
428+
throw Nep25Error("namedtype can only be used with Array type.");
429+
430+
EnsureValidNamedTypeIdentifier(NamedType);
431+
432+
if (Length.HasValue || ForbidNull.HasValue || Interface.HasValue || Key.HasValue || Value is not null || (Fields is not null && Fields.Length > 0))
433+
throw Nep25Error("namedtype cannot be combined with other modifiers.");
434+
435+
if (knownNamedTypes != null && !knownNamedTypes.Contains(NamedType))
436+
throw Nep25Error($"namedtype '{NamedType}' is not defined in the manifest.");
437+
}
438+
439+
if (Value is not null)
440+
{
441+
if (Type != ContractParameterType.Array && Type != ContractParameterType.InteropInterface && Type != ContractParameterType.Map)
442+
throw Nep25Error("value can only be specified for Array, Map or InteropInterface types.");
443+
444+
if (Fields is not null && Fields.Length > 0)
445+
throw Nep25Error("value and fields cannot be used together.");
446+
447+
if (Type == ContractParameterType.InteropInterface && !Interface.HasValue)
448+
throw Nep25Error("interface must be provided when value is specified for InteropInterface type.");
449+
450+
if (Type == ContractParameterType.Map && !Key.HasValue)
451+
throw Nep25Error("key must be provided when value is specified for Map type.");
452+
453+
Value.ValidateCore(expectedType: null, allowFields, knownNamedTypes, allowNamedTypeReference);
454+
}
455+
else
456+
{
457+
if (Type == ContractParameterType.Map)
458+
throw Nep25Error("value is required for Map type.");
459+
460+
if (Type == ContractParameterType.InteropInterface)
461+
throw Nep25Error("value is required for InteropInterface type.");
462+
463+
if (Type == ContractParameterType.Array && NamedType is null && (Fields is null || Fields.Length == 0))
464+
throw Nep25Error("value, namedtype or fields must be provided for Array type to describe element type.");
465+
}
466+
467+
if (Fields is not null && Fields.Length > 0)
468+
{
469+
if (!allowFields)
470+
throw Nep25Error("fields cannot be used in method parameters or return values.");
471+
472+
if (Type != ContractParameterType.Array)
473+
throw Nep25Error("fields can only be used with Array type.");
474+
475+
if (Value is not null)
476+
throw Nep25Error("fields and value cannot be used together.");
477+
478+
if (NamedType != null)
479+
throw Nep25Error("fields cannot be combined with namedtype.");
480+
481+
foreach (var field in Fields)
482+
{
483+
field.ExtendedType?.ValidateCore(field.Type, allowFields: true, knownNamedTypes, allowNamedTypeReference);
484+
}
485+
}
486+
487+
if (!allowFields)
488+
{
489+
if (Fields is not null && Fields.Length > 0)
490+
throw Nep25Error("fields cannot be used in method parameters or return values.");
491+
492+
if (Value?.Fields is { Length: > 0 })
493+
throw Nep25Error("fields cannot be used in method parameters or return values.");
494+
}
495+
}
332496
}
333497
#nullable disable
334498
}
335-

0 commit comments

Comments
 (0)