From 51f3239b8d407754dffd0a95de941536a19fdf44 Mon Sep 17 00:00:00 2001 From: jimmy Date: Thu, 23 Oct 2025 22:18:51 +0800 Subject: [PATCH 1/3] Expose policy governance APIs and strengthen analyzer UX --- .../CompilationEngine/CompilationContext.cs | 2 +- .../AssetBuilder/OptimizedScriptBuilder.cs | 88 +++++++++++--- src/Neo.Compiler.CSharp/Program.cs | 13 ++ .../SecurityAnalyzer/ReEntrancyAnalyzer.cs | 33 +++++- .../SecurityAnalyzer/WriteInTryAnalyzer.cs | 31 ++++- .../Native/Policy.cs | 10 ++ .../Native/RoleManagement.cs | 2 + .../Native/TransactionAttribute.cs | 111 ++++++++++++++++++ .../Native/Policy.cs | 12 ++ .../Contract_Native.cs | 12 ++ .../Services/NativeTest.cs | 6 + .../TestingArtifacts/Contract_Native.cs | 24 +++- 12 files changed, 319 insertions(+), 25 deletions(-) create mode 100644 src/Neo.SmartContract.Framework/Native/TransactionAttribute.cs diff --git a/src/Neo.Compiler.CSharp/CompilationEngine/CompilationContext.cs b/src/Neo.Compiler.CSharp/CompilationEngine/CompilationContext.cs index a935d6145..02fdf01d8 100644 --- a/src/Neo.Compiler.CSharp/CompilationEngine/CompilationContext.cs +++ b/src/Neo.Compiler.CSharp/CompilationEngine/CompilationContext.cs @@ -63,8 +63,8 @@ public class CompilationContext public bool Success => _diagnostics.All(p => p.Severity != DiagnosticSeverity.Error); public IReadOnlyList Diagnostics => _diagnostics; - // TODO: basename should not work when multiple contracts exit in one project public string? ContractName => _displayName ?? Options.BaseName ?? _className; + internal string ClassName => _className ?? _targetContract.Name; private string? Source { get; set; } internal IEnumerable StaticFieldSymbols => _staticFields.OrderBy(p => p.Value).Select(p => p.Key); diff --git a/src/Neo.Compiler.CSharp/Optimizer/AssetBuilder/OptimizedScriptBuilder.cs b/src/Neo.Compiler.CSharp/Optimizer/AssetBuilder/OptimizedScriptBuilder.cs index 705d6c640..f8af2a077 100644 --- a/src/Neo.Compiler.CSharp/Optimizer/AssetBuilder/OptimizedScriptBuilder.cs +++ b/src/Neo.Compiler.CSharp/Optimizer/AssetBuilder/OptimizedScriptBuilder.cs @@ -21,6 +21,22 @@ namespace Neo.Optimizer { static class OptimizedScriptBuilder { + private static readonly IReadOnlyDictionary ShortToLongJumpMap = + new Dictionary + { + { OpCode.JMP, (OpCode.JMP_L, 3) }, + { OpCode.JMPIF, (OpCode.JMPIF_L, 3) }, + { OpCode.JMPIFNOT, (OpCode.JMPIFNOT_L, 3) }, + { OpCode.JMPEQ, (OpCode.JMPEQ_L, 3) }, + { OpCode.JMPNE, (OpCode.JMPNE_L, 3) }, + { OpCode.JMPGT, (OpCode.JMPGT_L, 3) }, + { OpCode.JMPGE, (OpCode.JMPGE_L, 3) }, + { OpCode.JMPLT, (OpCode.JMPLT_L, 3) }, + { OpCode.JMPLE, (OpCode.JMPLE_L, 3) }, + { OpCode.CALL, (OpCode.CALL_L, 3) }, + { OpCode.ENDTRY, (OpCode.ENDTRY_L, 3) }, + }; + /// /// Build script with instruction dictionary and jump /// @@ -37,19 +53,27 @@ public static Script BuildScriptWithJumpTargets( Dictionary? oldAddressToInstruction = null) { List simplifiedScript = new(); + int instructionIndex = 0; foreach (DictionaryEntry item in simplifiedInstructionsToAddress) { (Instruction i, int a) = ((Instruction)item.Key, (int)item.Value!); - simplifiedScript.Add((byte)i.OpCode); - int operandSizeLength = OperandSizePrefixTable[(int)i.OpCode]; - simplifiedScript = simplifiedScript.Concat(BitConverter.GetBytes(i.Operand.Length)[0..operandSizeLength]).ToList(); - if (jumpSourceToTargets.TryGetValue(i, out Instruction? dst)) + bool operandEmitted = false; + OpCode opcodeToEmit = i.OpCode; + bool upgradedToLong = false; + int delta = 0; + jumpSourceToTargets.TryGetValue(i, out Instruction? maybeTarget); + bool hasJumpTarget = maybeTarget is not null; + if (hasJumpTarget) { - int delta; - if (simplifiedInstructionsToAddress.Contains(dst)) // target instruction not deleted + Instruction dst = maybeTarget!; + if (simplifiedInstructionsToAddress.Contains(dst)) + { delta = (int)simplifiedInstructionsToAddress[dst]! - a; + } else if (i.OpCode == OpCode.PUSHA || i.OpCode == OpCode.ENDTRY || i.OpCode == OpCode.ENDTRY_L) + { delta = 0; // TODO: decide a good target + } else { if (oldAddressToInstruction != null) @@ -58,15 +82,36 @@ public static Script BuildScriptWithJumpTargets( throw new BadScriptException($"Target instruction of {i.OpCode} at old address {oldAddress} is deleted"); throw new BadScriptException($"Target instruction of {i.OpCode} at new address {a} is deleted"); } - if (i.OpCode == OpCode.JMP || conditionalJump.Contains(i.OpCode) || i.OpCode == OpCode.CALL || i.OpCode == OpCode.ENDTRY) - if (sbyte.MinValue <= delta && delta <= sbyte.MaxValue) - simplifiedScript.Add(BitConverter.GetBytes(delta)[0]); - else - // TODO: build with _L version - throw new NotImplementedException($"Need {i.OpCode}_L for delta={delta}"); - if (i.OpCode == OpCode.PUSHA || i.OpCode == OpCode.JMP_L || conditionalJump_L.Contains(i.OpCode) || i.OpCode == OpCode.CALL_L || i.OpCode == OpCode.ENDTRY_L) + + if (ShortToLongJumpMap.TryGetValue(opcodeToEmit, out (OpCode LongOp, int SizeDelta) map) && (delta < sbyte.MinValue || delta > sbyte.MaxValue)) + { + opcodeToEmit = map.LongOp; + upgradedToLong = true; + AdjustInstructionAddresses(simplifiedInstructionsToAddress, instructionIndex + 1, map.SizeDelta); + if (simplifiedInstructionsToAddress.Contains(dst)) + delta = (int)simplifiedInstructionsToAddress[dst]! - a; + } + } + + simplifiedScript.Add((byte)opcodeToEmit); + int operandSizeLength = OperandSizePrefixTable[(int)opcodeToEmit]; + simplifiedScript = simplifiedScript.Concat(BitConverter.GetBytes(i.Operand.Length)[0..operandSizeLength]).ToList(); + if (hasJumpTarget) + { + if (opcodeToEmit == OpCode.JMP || conditionalJump.Contains(opcodeToEmit) || opcodeToEmit == OpCode.CALL || opcodeToEmit == OpCode.ENDTRY) + { + simplifiedScript.Add(BitConverter.GetBytes(delta)[0]); + operandEmitted = true; + } + else if (opcodeToEmit == OpCode.PUSHA || opcodeToEmit == OpCode.JMP_L || conditionalJump_L.Contains(opcodeToEmit) || opcodeToEmit == OpCode.CALL_L || opcodeToEmit == OpCode.ENDTRY_L) + { simplifiedScript = simplifiedScript.Concat(BitConverter.GetBytes(delta)).ToList(); - continue; + operandEmitted = true; + } + else if (upgradedToLong) + { + throw new NotImplementedException($"Long form emission missing handler for {opcodeToEmit}."); + } } if (trySourceToTargets.TryGetValue(i, out (Instruction dst1, Instruction dst2) dsts)) { @@ -82,15 +127,26 @@ public static Script BuildScriptWithJumpTargets( simplifiedScript = simplifiedScript.Concat(BitConverter.GetBytes(delta1)).ToList(); simplifiedScript = simplifiedScript.Concat(BitConverter.GetBytes(delta2)).ToList(); } - continue; + operandEmitted = true; } - if (i.Operand.Length != 0) + if (!operandEmitted && i.Operand.Length != 0) simplifiedScript = simplifiedScript.Concat(i.Operand.ToArray()).ToList(); + instructionIndex++; } Script script = new(simplifiedScript.ToArray()); return script; } + private static void AdjustInstructionAddresses(System.Collections.Specialized.OrderedDictionary instructionsToAddress, int startIndex, int sizeDelta) + { + if (sizeDelta == 0) return; + for (int idx = startIndex; idx < instructionsToAddress.Count; idx++) + { + int current = (int)instructionsToAddress[idx]!; + instructionsToAddress[idx] = current + sizeDelta; + } + } + /// /// Typically used when you delete the oldTarget from script /// and the newTarget is the first following instruction undeleted in script diff --git a/src/Neo.Compiler.CSharp/Program.cs b/src/Neo.Compiler.CSharp/Program.cs index 87f236b96..91a13c67b 100644 --- a/src/Neo.Compiler.CSharp/Program.cs +++ b/src/Neo.Compiler.CSharp/Program.cs @@ -390,6 +390,19 @@ private static int ProcessSources(Options options, string folder, string[] sourc private static int ProcessOutputs(Options options, string folder, List contexts) { + if (!string.IsNullOrEmpty(options.BaseName) && contexts.Count > 1) + { + string[] uniqueContracts = contexts + .Select(c => c.ClassName) + .Distinct(StringComparer.InvariantCulture) + .ToArray(); + if (uniqueContracts.Length > 1) + { + Console.Error.WriteLine("The --base-name option can only be used when compiling a single contract. Contracts found: {0}", string.Join(", ", uniqueContracts)); + return 1; + } + } + int result = 0; List exceptions = new(); foreach (CompilationContext context in contexts) diff --git a/src/Neo.Compiler.CSharp/SecurityAnalyzer/ReEntrancyAnalyzer.cs b/src/Neo.Compiler.CSharp/SecurityAnalyzer/ReEntrancyAnalyzer.cs index eed34d1ae..b2a5378e5 100644 --- a/src/Neo.Compiler.CSharp/SecurityAnalyzer/ReEntrancyAnalyzer.cs +++ b/src/Neo.Compiler.CSharp/SecurityAnalyzer/ReEntrancyAnalyzer.cs @@ -16,6 +16,7 @@ using Neo.SmartContract.Testing.Coverage; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; @@ -36,8 +37,6 @@ public class ReEntrancyVulnerabilityPair public readonly Dictionary> callOtherContractInstructions; public readonly Dictionary> writeStorageInstructions; public JToken? DebugInfo { get; init; } - // TODO: use debugInfo to GetWarningInfo with source codes - public ReEntrancyVulnerabilityPair( Dictionary> vulnerabilityPairs, Dictionary> callOtherContractInstructions, @@ -243,17 +242,45 @@ private class SourceLocation if (sequencePoint.Document >= 0 && sequencePoint.Document < debugInfo.Documents.Count) { var fileName = debugInfo.Documents[sequencePoint.Document]; + var snippet = TryGetSourceLine(fileName, sequencePoint.Start.Line); return new SourceLocation { FileName = System.IO.Path.GetFileName(fileName), Line = sequencePoint.Start.Line, Column = sequencePoint.Start.Column, - CodeSnippet = null // Could be enhanced to read actual source code + CodeSnippet = snippet }; } } } return null; } + + private static string? TryGetSourceLine(string fileName, int zeroBasedLineNumber) + { + string? path = fileName; + if (!Path.IsPathRooted(path)) + { + path = Path.Combine(Environment.CurrentDirectory, path); + } + + try + { + if (path != null && File.Exists(path)) + { + string[] lines = File.ReadAllLines(path); + foreach (int candidate in new[] { zeroBasedLineNumber, zeroBasedLineNumber - 1 }) + { + if (candidate >= 0 && candidate < lines.Length) + return lines[candidate].Trim(); + } + } + } + catch + { + // ignore IO issues and fall back to raw offsets + } + return null; + } } } diff --git a/src/Neo.Compiler.CSharp/SecurityAnalyzer/WriteInTryAnalyzer.cs b/src/Neo.Compiler.CSharp/SecurityAnalyzer/WriteInTryAnalyzer.cs index f8d83d183..26a4f29bd 100644 --- a/src/Neo.Compiler.CSharp/SecurityAnalyzer/WriteInTryAnalyzer.cs +++ b/src/Neo.Compiler.CSharp/SecurityAnalyzer/WriteInTryAnalyzer.cs @@ -16,6 +16,7 @@ using Neo.SmartContract.Testing.Coverage; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; @@ -28,8 +29,6 @@ public class WriteInTryVulnerability // key block writes storage; value blocks in try public readonly Dictionary> vulnerabilities; public JToken? DebugInfo { get; init; } - // TODO: use debugInfo to GetWarningInfo with source codes - public WriteInTryVulnerability(Dictionary> vulnerabilities, JToken? debugInfo = null) { this.vulnerabilities = vulnerabilities; @@ -211,12 +210,13 @@ private class SourceLocation if (sequencePoint.Document >= 0 && sequencePoint.Document < debugInfo.Documents.Count) { var fileName = debugInfo.Documents[sequencePoint.Document]; + var snippet = TryGetSourceLine(fileName, sequencePoint.Start.Line); return new SourceLocation { FileName = System.IO.Path.GetFileName(fileName), Line = sequencePoint.Start.Line, Column = sequencePoint.Start.Column, - CodeSnippet = null // Could be enhanced to read actual source code + CodeSnippet = snippet }; } } @@ -224,6 +224,31 @@ private class SourceLocation return null; } + private static string? TryGetSourceLine(string fileName, int zeroBasedLineNumber) + { + string? path = fileName; + if (!Path.IsPathRooted(path)) + path = Path.Combine(Environment.CurrentDirectory, path); + + try + { + if (path != null && File.Exists(path)) + { + string[] lines = File.ReadAllLines(path); + foreach (int candidate in new[] { zeroBasedLineNumber, zeroBasedLineNumber - 1 }) + { + if (candidate >= 0 && candidate < lines.Length) + return lines[candidate].Trim(); + } + } + } + catch + { + // ignore IO failures, diagnostics will fall back to instruction offsets + } + return null; + } + public static HashSet FindAllBasicBlocksWritingStorageInTryCatchFinally (TryCatchFinallySingleCoverage c, HashSet visitedTrys, HashSet allBasicBlocksWritingStorage) { diff --git a/src/Neo.SmartContract.Framework/Native/Policy.cs b/src/Neo.SmartContract.Framework/Native/Policy.cs index ddc6c0a8d..7d0e504af 100644 --- a/src/Neo.SmartContract.Framework/Native/Policy.cs +++ b/src/Neo.SmartContract.Framework/Native/Policy.cs @@ -12,6 +12,7 @@ #pragma warning disable CS0626 using Neo.SmartContract.Framework.Attributes; +using Neo.SmartContract.Framework; namespace Neo.SmartContract.Framework.Native { @@ -26,5 +27,14 @@ public class Policy public static extern bool IsBlocked(UInt160 account); public static extern uint GetAttributeFee(TransactionAttributeType attributeType); public static extern void SetAttributeFee(TransactionAttributeType attributeType, uint value); + public static extern void SetFeePerByte(long value); + public static extern void SetExecFeeFactor(uint value); + public static extern void SetStoragePrice(uint value); + public static extern bool BlockAccount(UInt160 account); + public static extern bool UnblockAccount(UInt160 account); + public static extern uint GetMaxValidUntilBlockIncrement(); + public static extern void SetMaxValidUntilBlockIncrement(uint value); + public static extern uint GetMaxTraceableBlocks(); + public static extern void SetMaxTraceableBlocks(uint value); } } diff --git a/src/Neo.SmartContract.Framework/Native/RoleManagement.cs b/src/Neo.SmartContract.Framework/Native/RoleManagement.cs index 72d3a63fc..b369ff82c 100644 --- a/src/Neo.SmartContract.Framework/Native/RoleManagement.cs +++ b/src/Neo.SmartContract.Framework/Native/RoleManagement.cs @@ -12,6 +12,7 @@ #pragma warning disable CS0626 using Neo.SmartContract.Framework.Attributes; +using Neo.SmartContract.Framework; namespace Neo.SmartContract.Framework.Native { @@ -21,5 +22,6 @@ public class RoleManagement [ContractHash] public static extern UInt160 Hash { get; } public static extern ECPoint[] GetDesignatedByRole(Role role, uint index); + public static extern void DesignateAsRole(Role role, ECPoint[] nodes); } } diff --git a/src/Neo.SmartContract.Framework/Native/TransactionAttribute.cs b/src/Neo.SmartContract.Framework/Native/TransactionAttribute.cs new file mode 100644 index 000000000..de683335d --- /dev/null +++ b/src/Neo.SmartContract.Framework/Native/TransactionAttribute.cs @@ -0,0 +1,111 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// TransactionAttribute.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.SmartContract.Framework; + +namespace Neo.SmartContract.Framework.Native +{ + /// + /// Base type for transaction attributes exposed inside smart contracts. + /// + public abstract class TransactionAttribute + { + /// + /// The attribute type. + /// + public TransactionAttributeType Type { get; set; } + } + + /// + /// Marks a transaction as high priority. + /// + public sealed class HighPriority : TransactionAttribute + { + public HighPriority() + { + Type = TransactionAttributeType.HighPriority; + } + } + + /// + /// Declares that a transaction represents an oracle response. + /// + public sealed class OracleResponse : TransactionAttribute + { + public OracleResponse() + { + Type = TransactionAttributeType.OracleResponse; + } + + /// + /// The oracle request identifier. + /// + public ulong Id { get; set; } + + /// + /// Oracle execution result code. + /// + public OracleResponseCode Code { get; set; } + + /// + /// Payload returned by the oracle. + /// + public ByteString Result { get; set; } = null!; + } + + /// + /// Prevents a transaction from being accepted before a certain block height. + /// + public sealed class NotValidBefore : TransactionAttribute + { + public NotValidBefore() + { + Type = TransactionAttributeType.NotValidBefore; + } + + /// + /// Minimum block height at which the transaction becomes valid. + /// + public uint Height { get; set; } + } + + /// + /// Declares a mutual exclusion relationship between transactions. + /// + public sealed class Conflicts : TransactionAttribute + { + public Conflicts() + { + Type = TransactionAttributeType.Conflicts; + } + + /// + /// Hash of the conflicting transaction. + /// + public UInt256 Hash { get; set; } = null!; + } + + /// + /// Indicates that the transaction is assisted by the notary service. + /// + public sealed class NotaryAssisted : TransactionAttribute + { + public NotaryAssisted() + { + Type = TransactionAttributeType.NotaryAssisted; + } + + /// + /// Number of keys that are expected to participate in the assisted signing flow. + /// + public byte NKeys { get; set; } + } +} diff --git a/src/Neo.SmartContract.Testing/Native/Policy.cs b/src/Neo.SmartContract.Testing/Native/Policy.cs index 8d4ca8b21..823ced34c 100644 --- a/src/Neo.SmartContract.Testing/Native/Policy.cs +++ b/src/Neo.SmartContract.Testing/Native/Policy.cs @@ -57,6 +57,18 @@ public abstract class Policy(SmartContractInitialize initialize) : SmartContract [DisplayName("isBlocked")] public abstract bool IsBlocked(UInt160 account); + /// + /// Safe method + /// + [DisplayName("getMaxValidUntilBlockIncrement")] + public abstract uint GetMaxValidUntilBlockIncrement(); + + /// + /// Safe method + /// + [DisplayName("getMaxTraceableBlocks")] + public abstract uint GetMaxTraceableBlocks(); + #endregion #region Unsafe methods diff --git a/tests/Neo.SmartContract.Framework.TestContracts/Contract_Native.cs b/tests/Neo.SmartContract.Framework.TestContracts/Contract_Native.cs index 461316b1e..2b7e842c7 100644 --- a/tests/Neo.SmartContract.Framework.TestContracts/Contract_Native.cs +++ b/tests/Neo.SmartContract.Framework.TestContracts/Contract_Native.cs @@ -84,5 +84,17 @@ public static bool Policy_IsBlocked(UInt160 account) { return Policy.IsBlocked(account); } + + [DisplayName("Policy_GetMaxValidUntilBlockIncrement")] + public static uint Policy_GetMaxValidUntilBlockIncrement() + { + return Policy.GetMaxValidUntilBlockIncrement(); + } + + [DisplayName("Policy_GetMaxTraceableBlocks")] + public static uint Policy_GetMaxTraceableBlocks() + { + return Policy.GetMaxTraceableBlocks(); + } } } diff --git a/tests/Neo.SmartContract.Framework.UnitTests/Services/NativeTest.cs b/tests/Neo.SmartContract.Framework.UnitTests/Services/NativeTest.cs index 46c5d4a33..1a6c67aef 100644 --- a/tests/Neo.SmartContract.Framework.UnitTests/Services/NativeTest.cs +++ b/tests/Neo.SmartContract.Framework.UnitTests/Services/NativeTest.cs @@ -10,12 +10,14 @@ // modifications are permitted. using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo; using Neo.Cryptography.ECC; using Neo.Network.P2P.Payloads; using Neo.SmartContract.Testing; using Neo.SmartContract.Testing.Extensions; using Neo.VM.Types; using System.Linq; +using System.Numerics; namespace Neo.SmartContract.Framework.UnitTests.Services { @@ -56,6 +58,10 @@ public void Test_Policy() { Assert.AreEqual(1000L, Contract.Policy_GetFeePerByte()); Assert.IsFalse(Contract.Policy_IsBlocked(Alice.Account)); + var maxValidUntilBlockIncrement = Engine.Native.Policy.GetMaxValidUntilBlockIncrement(); + var maxTraceableBlocks = Engine.Native.Policy.GetMaxTraceableBlocks(); + Assert.AreEqual((BigInteger)maxValidUntilBlockIncrement, Contract.Policy_GetMaxValidUntilBlockIncrement()!.Value); + Assert.AreEqual((BigInteger)maxTraceableBlocks, Contract.Policy_GetMaxTraceableBlocks()!.Value); } } } diff --git a/tests/Neo.SmartContract.Framework.UnitTests/TestingArtifacts/Contract_Native.cs b/tests/Neo.SmartContract.Framework.UnitTests/TestingArtifacts/Contract_Native.cs index b7198ce9b..dbb92f4c3 100644 --- a/tests/Neo.SmartContract.Framework.UnitTests/TestingArtifacts/Contract_Native.cs +++ b/tests/Neo.SmartContract.Framework.UnitTests/TestingArtifacts/Contract_Native.cs @@ -11,12 +11,12 @@ public abstract class Contract_Native(Neo.SmartContract.Testing.SmartContractIni { #region Compiled data - public static Neo.SmartContract.Manifest.ContractManifest Manifest => Neo.SmartContract.Manifest.ContractManifest.Parse(@"{""name"":""Contract_Native"",""groups"":[],""features"":{},""supportedstandards"":[],""abi"":{""methods"":[{""name"":""NEO_Decimals"",""parameters"":[],""returntype"":""Integer"",""offset"":0,""safe"":false},{""name"":""NEO_Transfer"",""parameters"":[{""name"":""from"",""type"":""Hash160""},{""name"":""to"",""type"":""Hash160""},{""name"":""amount"",""type"":""Integer""}],""returntype"":""Boolean"",""offset"":4,""safe"":false},{""name"":""NEO_BalanceOf"",""parameters"":[{""name"":""account"",""type"":""Hash160""}],""returntype"":""Integer"",""offset"":15,""safe"":false},{""name"":""NEO_GetAccountState"",""parameters"":[{""name"":""account"",""type"":""Hash160""}],""returntype"":""Any"",""offset"":23,""safe"":false},{""name"":""NEO_GetGasPerBlock"",""parameters"":[],""returntype"":""Integer"",""offset"":31,""safe"":false},{""name"":""NEO_UnclaimedGas"",""parameters"":[{""name"":""account"",""type"":""Hash160""},{""name"":""end"",""type"":""Integer""}],""returntype"":""Integer"",""offset"":35,""safe"":false},{""name"":""NEO_RegisterCandidate"",""parameters"":[{""name"":""pubkey"",""type"":""PublicKey""}],""returntype"":""Boolean"",""offset"":44,""safe"":false},{""name"":""NEO_GetCandidates"",""parameters"":[],""returntype"":""Array"",""offset"":52,""safe"":false},{""name"":""GAS_Decimals"",""parameters"":[],""returntype"":""Integer"",""offset"":56,""safe"":false},{""name"":""Policy_GetFeePerByte"",""parameters"":[],""returntype"":""Integer"",""offset"":60,""safe"":false},{""name"":""Policy_IsBlocked"",""parameters"":[{""name"":""account"",""type"":""Hash160""}],""returntype"":""Boolean"",""offset"":64,""safe"":false}],""events"":[]},""permissions"":[{""contract"":""0xcc5e4edd9f5f8dba8bb65734541df7a1c081c67b"",""methods"":[""getFeePerByte"",""isBlocked""]},{""contract"":""0xd2a4cff31913016155e38e474a2c06d08be276cf"",""methods"":[""decimals""]},{""contract"":""0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5"",""methods"":[""balanceOf"",""decimals"",""getAccountState"",""getCandidates"",""getGasPerBlock"",""registerCandidate"",""transfer"",""unclaimedGas""]}],""trusts"":[],""extra"":{""Version"":""3.8.1"",""nef"":{""optimization"":""All""}}}"); + public static Neo.SmartContract.Manifest.ContractManifest Manifest => Neo.SmartContract.Manifest.ContractManifest.Parse(@"{""name"":""Contract_Native"",""groups"":[],""features"":{},""supportedstandards"":[],""abi"":{""methods"":[{""name"":""NEO_Decimals"",""parameters"":[],""returntype"":""Integer"",""offset"":0,""safe"":false},{""name"":""NEO_Transfer"",""parameters"":[{""name"":""from"",""type"":""Hash160""},{""name"":""to"",""type"":""Hash160""},{""name"":""amount"",""type"":""Integer""}],""returntype"":""Boolean"",""offset"":4,""safe"":false},{""name"":""NEO_BalanceOf"",""parameters"":[{""name"":""account"",""type"":""Hash160""}],""returntype"":""Integer"",""offset"":15,""safe"":false},{""name"":""NEO_GetAccountState"",""parameters"":[{""name"":""account"",""type"":""Hash160""}],""returntype"":""Any"",""offset"":23,""safe"":false},{""name"":""NEO_GetGasPerBlock"",""parameters"":[],""returntype"":""Integer"",""offset"":31,""safe"":false},{""name"":""NEO_UnclaimedGas"",""parameters"":[{""name"":""account"",""type"":""Hash160""},{""name"":""end"",""type"":""Integer""}],""returntype"":""Integer"",""offset"":35,""safe"":false},{""name"":""NEO_RegisterCandidate"",""parameters"":[{""name"":""pubkey"",""type"":""PublicKey""}],""returntype"":""Boolean"",""offset"":44,""safe"":false},{""name"":""NEO_GetCandidates"",""parameters"":[],""returntype"":""Array"",""offset"":52,""safe"":false},{""name"":""GAS_Decimals"",""parameters"":[],""returntype"":""Integer"",""offset"":56,""safe"":false},{""name"":""Policy_GetFeePerByte"",""parameters"":[],""returntype"":""Integer"",""offset"":60,""safe"":false},{""name"":""Policy_IsBlocked"",""parameters"":[{""name"":""account"",""type"":""Hash160""}],""returntype"":""Boolean"",""offset"":64,""safe"":false},{""name"":""Policy_GetMaxValidUntilBlockIncrement"",""parameters"":[],""returntype"":""Integer"",""offset"":72,""safe"":false},{""name"":""Policy_GetMaxTraceableBlocks"",""parameters"":[],""returntype"":""Integer"",""offset"":76,""safe"":false}],""events"":[]},""permissions"":[{""contract"":""0xcc5e4edd9f5f8dba8bb65734541df7a1c081c67b"",""methods"":[""getFeePerByte"",""getMaxTraceableBlocks"",""getMaxValidUntilBlockIncrement"",""isBlocked""]},{""contract"":""0xd2a4cff31913016155e38e474a2c06d08be276cf"",""methods"":[""decimals""]},{""contract"":""0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5"",""methods"":[""balanceOf"",""decimals"",""getAccountState"",""getCandidates"",""getGasPerBlock"",""registerCandidate"",""transfer"",""unclaimedGas""]}],""trusts"":[],""extra"":{""Version"":""3.8.1"",""nef"":{""optimization"":""All""}}}"); /// /// Optimization: "All" /// - public static Neo.SmartContract.NefFile Nef => Convert.FromBase64String(@"TkVGM1Rlc3RpbmdFbmdpbmUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAv1Y+pAvCg9TQ4FxI6jBbPyoHNA7whkZWNpbWFscwAAAQ/1Y+pAvCg9TQ4FxI6jBbPyoHNA7wh0cmFuc2ZlcgQAAQ/1Y+pAvCg9TQ4FxI6jBbPyoHNA7wliYWxhbmNlT2YBAAEP9WPqQLwoPU0OBcSOowWz8qBzQO8PZ2V0QWNjb3VudFN0YXRlAQABD/Vj6kC8KD1NDgXEjqMFs/Kgc0DvDmdldEdhc1BlckJsb2NrAAABD/Vj6kC8KD1NDgXEjqMFs/Kgc0DvDHVuY2xhaW1lZEdhcwIAAQ/1Y+pAvCg9TQ4FxI6jBbPyoHNA7xFyZWdpc3RlckNhbmRpZGF0ZQEAAQ/1Y+pAvCg9TQ4FxI6jBbPyoHNA7w1nZXRDYW5kaWRhdGVzAAABD8924ovQBixKR47jVWEBExnzz6TSCGRlY2ltYWxzAAABD3vGgcCh9x1UNFe2i7qNX5/dTl7MDWdldEZlZVBlckJ5dGUAAAEPe8aBwKH3HVQ0V7aLuo1fn91OXswJaXNCbG9ja2VkAQABDwAASDcAAEBXAAMLenl4NwEAQFcAAXg3AgBAVwABeDcDAEA3BABAVwACeXg3BQBAVwABeDcGAEA3BwBANwgAQDcJAEBXAAF4NwoAQL+gUWg=").AsSerializable(); + public static Neo.SmartContract.NefFile Nef => Convert.FromBase64String(@"TkVGM1Rlc3RpbmdFbmdpbmUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA31Y+pAvCg9TQ4FxI6jBbPyoHNA7whkZWNpbWFscwAAAQ/1Y+pAvCg9TQ4FxI6jBbPyoHNA7wh0cmFuc2ZlcgQAAQ/1Y+pAvCg9TQ4FxI6jBbPyoHNA7wliYWxhbmNlT2YBAAEP9WPqQLwoPU0OBcSOowWz8qBzQO8PZ2V0QWNjb3VudFN0YXRlAQABD/Vj6kC8KD1NDgXEjqMFs/Kgc0DvDmdldEdhc1BlckJsb2NrAAABD/Vj6kC8KD1NDgXEjqMFs/Kgc0DvDHVuY2xhaW1lZEdhcwIAAQ/1Y+pAvCg9TQ4FxI6jBbPyoHNA7xFyZWdpc3RlckNhbmRpZGF0ZQEAAQ/1Y+pAvCg9TQ4FxI6jBbPyoHNA7w1nZXRDYW5kaWRhdGVzAAABD8924ovQBixKR47jVWEBExnzz6TSCGRlY2ltYWxzAAABD3vGgcCh9x1UNFe2i7qNX5/dTl7MDWdldEZlZVBlckJ5dGUAAAEPe8aBwKH3HVQ0V7aLuo1fn91OXswJaXNCbG9ja2VkAQABD3vGgcCh9x1UNFe2i7qNX5/dTl7MHmdldE1heFZhbGlkVW50aWxCbG9ja0luY3JlbWVudAAAAQ97xoHAofcdVDRXtou6jV+f3U5ezBVnZXRNYXhUcmFjZWFibGVCbG9ja3MAAAEPAABQNwAAQFcAAwt6eXg3AQBAVwABeDcCAEBXAAF4NwMAQDcEAEBXAAJ5eDcFAEBXAAF4NwYAQDcHAEA3CABANwkAQFcAAXg3CgBANwsAQDcMAEA2ACxb").AsSerializable(); #endregion @@ -136,6 +136,26 @@ public abstract class Contract_Native(Neo.SmartContract.Testing.SmartContractIni /// public abstract BigInteger? Policy_GetFeePerByte(); + /// + /// Unsafe method + /// + /// + /// Script: NwwAQA== + /// CALLT 0C00 [32768 datoshi] + /// RET [0 datoshi] + /// + public abstract BigInteger? Policy_GetMaxTraceableBlocks(); + + /// + /// Unsafe method + /// + /// + /// Script: NwsAQA== + /// CALLT 0B00 [32768 datoshi] + /// RET [0 datoshi] + /// + public abstract BigInteger? Policy_GetMaxValidUntilBlockIncrement(); + /// /// Unsafe method /// From 0412a3e4eab9808e569d4198f925a98a459b5567 Mon Sep 17 00:00:00 2001 From: jimmy Date: Fri, 24 Oct 2025 01:47:31 +0800 Subject: [PATCH 2/3] Return full source span in analyzer snippets --- .../SecurityAnalyzer/ReEntrancyAnalyzer.cs | 13 ++++++------- .../SecurityAnalyzer/WriteInTryAnalyzer.cs | 13 ++++++------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/Neo.Compiler.CSharp/SecurityAnalyzer/ReEntrancyAnalyzer.cs b/src/Neo.Compiler.CSharp/SecurityAnalyzer/ReEntrancyAnalyzer.cs index b2a5378e5..90000254f 100644 --- a/src/Neo.Compiler.CSharp/SecurityAnalyzer/ReEntrancyAnalyzer.cs +++ b/src/Neo.Compiler.CSharp/SecurityAnalyzer/ReEntrancyAnalyzer.cs @@ -242,7 +242,7 @@ private class SourceLocation if (sequencePoint.Document >= 0 && sequencePoint.Document < debugInfo.Documents.Count) { var fileName = debugInfo.Documents[sequencePoint.Document]; - var snippet = TryGetSourceLine(fileName, sequencePoint.Start.Line); + var snippet = TryGetSourceSnippet(fileName, sequencePoint.Start.Line, sequencePoint.End.Line); return new SourceLocation { FileName = System.IO.Path.GetFileName(fileName), @@ -256,7 +256,7 @@ private class SourceLocation return null; } - private static string? TryGetSourceLine(string fileName, int zeroBasedLineNumber) + private static string? TryGetSourceSnippet(string fileName, int startZeroBasedLineNumber, int endZeroBasedLineNumber) { string? path = fileName; if (!Path.IsPathRooted(path)) @@ -269,11 +269,10 @@ private class SourceLocation if (path != null && File.Exists(path)) { string[] lines = File.ReadAllLines(path); - foreach (int candidate in new[] { zeroBasedLineNumber, zeroBasedLineNumber - 1 }) - { - if (candidate >= 0 && candidate < lines.Length) - return lines[candidate].Trim(); - } + int start = Math.Max(0, startZeroBasedLineNumber); + int end = Math.Min(lines.Length - 1, Math.Max(startZeroBasedLineNumber, endZeroBasedLineNumber)); + if (start > end) return null; + return string.Join(Environment.NewLine, lines[start..(end + 1)]).Trim(); } } catch diff --git a/src/Neo.Compiler.CSharp/SecurityAnalyzer/WriteInTryAnalyzer.cs b/src/Neo.Compiler.CSharp/SecurityAnalyzer/WriteInTryAnalyzer.cs index 26a4f29bd..aaabc57f3 100644 --- a/src/Neo.Compiler.CSharp/SecurityAnalyzer/WriteInTryAnalyzer.cs +++ b/src/Neo.Compiler.CSharp/SecurityAnalyzer/WriteInTryAnalyzer.cs @@ -210,7 +210,7 @@ private class SourceLocation if (sequencePoint.Document >= 0 && sequencePoint.Document < debugInfo.Documents.Count) { var fileName = debugInfo.Documents[sequencePoint.Document]; - var snippet = TryGetSourceLine(fileName, sequencePoint.Start.Line); + var snippet = TryGetSourceSnippet(fileName, sequencePoint.Start.Line, sequencePoint.End.Line); return new SourceLocation { FileName = System.IO.Path.GetFileName(fileName), @@ -224,7 +224,7 @@ private class SourceLocation return null; } - private static string? TryGetSourceLine(string fileName, int zeroBasedLineNumber) + private static string? TryGetSourceSnippet(string fileName, int startZeroBasedLineNumber, int endZeroBasedLineNumber) { string? path = fileName; if (!Path.IsPathRooted(path)) @@ -235,11 +235,10 @@ private class SourceLocation if (path != null && File.Exists(path)) { string[] lines = File.ReadAllLines(path); - foreach (int candidate in new[] { zeroBasedLineNumber, zeroBasedLineNumber - 1 }) - { - if (candidate >= 0 && candidate < lines.Length) - return lines[candidate].Trim(); - } + int start = Math.Max(0, startZeroBasedLineNumber); + int end = Math.Min(lines.Length - 1, Math.Max(startZeroBasedLineNumber, endZeroBasedLineNumber)); + if (start > end) return null; + return string.Join(Environment.NewLine, lines[start..(end + 1)]).Trim(); } } catch From abdeae7c46f62b723d413c3b6141d6c9ded5928e Mon Sep 17 00:00:00 2001 From: jimmy Date: Fri, 24 Oct 2025 10:53:35 +0800 Subject: [PATCH 3/3] Remove non-policy compiler changes --- .../CompilationEngine/CompilationContext.cs | 1 - .../AssetBuilder/OptimizedScriptBuilder.cs | 88 ++++--------------- src/Neo.Compiler.CSharp/Program.cs | 13 --- .../SecurityAnalyzer/ReEntrancyAnalyzer.cs | 1 + .../SecurityAnalyzer/WriteInTryAnalyzer.cs | 1 + 5 files changed, 18 insertions(+), 86 deletions(-) diff --git a/src/Neo.Compiler.CSharp/CompilationEngine/CompilationContext.cs b/src/Neo.Compiler.CSharp/CompilationEngine/CompilationContext.cs index 437e54055..8e9fd37b0 100644 --- a/src/Neo.Compiler.CSharp/CompilationEngine/CompilationContext.cs +++ b/src/Neo.Compiler.CSharp/CompilationEngine/CompilationContext.cs @@ -64,7 +64,6 @@ public class CompilationContext public bool Success => _diagnostics.All(p => p.Severity != DiagnosticSeverity.Error); public IReadOnlyList Diagnostics => _diagnostics; public string? ContractName => _displayName ?? (_allowBaseName ? Options.BaseName : null) ?? _className; - internal string ClassName => _className ?? _targetContract.Name; private string? Source { get; set; } internal IEnumerable StaticFieldSymbols => _staticFields.OrderBy(p => p.Value).Select(p => p.Key); diff --git a/src/Neo.Compiler.CSharp/Optimizer/AssetBuilder/OptimizedScriptBuilder.cs b/src/Neo.Compiler.CSharp/Optimizer/AssetBuilder/OptimizedScriptBuilder.cs index f8af2a077..705d6c640 100644 --- a/src/Neo.Compiler.CSharp/Optimizer/AssetBuilder/OptimizedScriptBuilder.cs +++ b/src/Neo.Compiler.CSharp/Optimizer/AssetBuilder/OptimizedScriptBuilder.cs @@ -21,22 +21,6 @@ namespace Neo.Optimizer { static class OptimizedScriptBuilder { - private static readonly IReadOnlyDictionary ShortToLongJumpMap = - new Dictionary - { - { OpCode.JMP, (OpCode.JMP_L, 3) }, - { OpCode.JMPIF, (OpCode.JMPIF_L, 3) }, - { OpCode.JMPIFNOT, (OpCode.JMPIFNOT_L, 3) }, - { OpCode.JMPEQ, (OpCode.JMPEQ_L, 3) }, - { OpCode.JMPNE, (OpCode.JMPNE_L, 3) }, - { OpCode.JMPGT, (OpCode.JMPGT_L, 3) }, - { OpCode.JMPGE, (OpCode.JMPGE_L, 3) }, - { OpCode.JMPLT, (OpCode.JMPLT_L, 3) }, - { OpCode.JMPLE, (OpCode.JMPLE_L, 3) }, - { OpCode.CALL, (OpCode.CALL_L, 3) }, - { OpCode.ENDTRY, (OpCode.ENDTRY_L, 3) }, - }; - /// /// Build script with instruction dictionary and jump /// @@ -53,27 +37,19 @@ public static Script BuildScriptWithJumpTargets( Dictionary? oldAddressToInstruction = null) { List simplifiedScript = new(); - int instructionIndex = 0; foreach (DictionaryEntry item in simplifiedInstructionsToAddress) { (Instruction i, int a) = ((Instruction)item.Key, (int)item.Value!); - bool operandEmitted = false; - OpCode opcodeToEmit = i.OpCode; - bool upgradedToLong = false; - int delta = 0; - jumpSourceToTargets.TryGetValue(i, out Instruction? maybeTarget); - bool hasJumpTarget = maybeTarget is not null; - if (hasJumpTarget) + simplifiedScript.Add((byte)i.OpCode); + int operandSizeLength = OperandSizePrefixTable[(int)i.OpCode]; + simplifiedScript = simplifiedScript.Concat(BitConverter.GetBytes(i.Operand.Length)[0..operandSizeLength]).ToList(); + if (jumpSourceToTargets.TryGetValue(i, out Instruction? dst)) { - Instruction dst = maybeTarget!; - if (simplifiedInstructionsToAddress.Contains(dst)) - { + int delta; + if (simplifiedInstructionsToAddress.Contains(dst)) // target instruction not deleted delta = (int)simplifiedInstructionsToAddress[dst]! - a; - } else if (i.OpCode == OpCode.PUSHA || i.OpCode == OpCode.ENDTRY || i.OpCode == OpCode.ENDTRY_L) - { delta = 0; // TODO: decide a good target - } else { if (oldAddressToInstruction != null) @@ -82,36 +58,15 @@ public static Script BuildScriptWithJumpTargets( throw new BadScriptException($"Target instruction of {i.OpCode} at old address {oldAddress} is deleted"); throw new BadScriptException($"Target instruction of {i.OpCode} at new address {a} is deleted"); } - - if (ShortToLongJumpMap.TryGetValue(opcodeToEmit, out (OpCode LongOp, int SizeDelta) map) && (delta < sbyte.MinValue || delta > sbyte.MaxValue)) - { - opcodeToEmit = map.LongOp; - upgradedToLong = true; - AdjustInstructionAddresses(simplifiedInstructionsToAddress, instructionIndex + 1, map.SizeDelta); - if (simplifiedInstructionsToAddress.Contains(dst)) - delta = (int)simplifiedInstructionsToAddress[dst]! - a; - } - } - - simplifiedScript.Add((byte)opcodeToEmit); - int operandSizeLength = OperandSizePrefixTable[(int)opcodeToEmit]; - simplifiedScript = simplifiedScript.Concat(BitConverter.GetBytes(i.Operand.Length)[0..operandSizeLength]).ToList(); - if (hasJumpTarget) - { - if (opcodeToEmit == OpCode.JMP || conditionalJump.Contains(opcodeToEmit) || opcodeToEmit == OpCode.CALL || opcodeToEmit == OpCode.ENDTRY) - { - simplifiedScript.Add(BitConverter.GetBytes(delta)[0]); - operandEmitted = true; - } - else if (opcodeToEmit == OpCode.PUSHA || opcodeToEmit == OpCode.JMP_L || conditionalJump_L.Contains(opcodeToEmit) || opcodeToEmit == OpCode.CALL_L || opcodeToEmit == OpCode.ENDTRY_L) - { + if (i.OpCode == OpCode.JMP || conditionalJump.Contains(i.OpCode) || i.OpCode == OpCode.CALL || i.OpCode == OpCode.ENDTRY) + if (sbyte.MinValue <= delta && delta <= sbyte.MaxValue) + simplifiedScript.Add(BitConverter.GetBytes(delta)[0]); + else + // TODO: build with _L version + throw new NotImplementedException($"Need {i.OpCode}_L for delta={delta}"); + if (i.OpCode == OpCode.PUSHA || i.OpCode == OpCode.JMP_L || conditionalJump_L.Contains(i.OpCode) || i.OpCode == OpCode.CALL_L || i.OpCode == OpCode.ENDTRY_L) simplifiedScript = simplifiedScript.Concat(BitConverter.GetBytes(delta)).ToList(); - operandEmitted = true; - } - else if (upgradedToLong) - { - throw new NotImplementedException($"Long form emission missing handler for {opcodeToEmit}."); - } + continue; } if (trySourceToTargets.TryGetValue(i, out (Instruction dst1, Instruction dst2) dsts)) { @@ -127,26 +82,15 @@ public static Script BuildScriptWithJumpTargets( simplifiedScript = simplifiedScript.Concat(BitConverter.GetBytes(delta1)).ToList(); simplifiedScript = simplifiedScript.Concat(BitConverter.GetBytes(delta2)).ToList(); } - operandEmitted = true; + continue; } - if (!operandEmitted && i.Operand.Length != 0) + if (i.Operand.Length != 0) simplifiedScript = simplifiedScript.Concat(i.Operand.ToArray()).ToList(); - instructionIndex++; } Script script = new(simplifiedScript.ToArray()); return script; } - private static void AdjustInstructionAddresses(System.Collections.Specialized.OrderedDictionary instructionsToAddress, int startIndex, int sizeDelta) - { - if (sizeDelta == 0) return; - for (int idx = startIndex; idx < instructionsToAddress.Count; idx++) - { - int current = (int)instructionsToAddress[idx]!; - instructionsToAddress[idx] = current + sizeDelta; - } - } - /// /// Typically used when you delete the oldTarget from script /// and the newTarget is the first following instruction undeleted in script diff --git a/src/Neo.Compiler.CSharp/Program.cs b/src/Neo.Compiler.CSharp/Program.cs index 189d10f97..a2fc71f36 100644 --- a/src/Neo.Compiler.CSharp/Program.cs +++ b/src/Neo.Compiler.CSharp/Program.cs @@ -391,19 +391,6 @@ private static int ProcessSources(Options options, string folder, string[] sourc private static int ProcessOutputs(Options options, string folder, List contexts) { - if (!string.IsNullOrEmpty(options.BaseName) && contexts.Count > 1) - { - string[] uniqueContracts = contexts - .Select(c => c.ClassName) - .Distinct(StringComparer.InvariantCulture) - .ToArray(); - if (uniqueContracts.Length > 1) - { - Console.Error.WriteLine("The --base-name option can only be used when compiling a single contract. Contracts found: {0}", string.Join(", ", uniqueContracts)); - return 1; - } - } - int result = 0; List exceptions = new(); foreach (CompilationContext context in contexts) diff --git a/src/Neo.Compiler.CSharp/SecurityAnalyzer/ReEntrancyAnalyzer.cs b/src/Neo.Compiler.CSharp/SecurityAnalyzer/ReEntrancyAnalyzer.cs index 6b39c58f4..284e371b5 100644 --- a/src/Neo.Compiler.CSharp/SecurityAnalyzer/ReEntrancyAnalyzer.cs +++ b/src/Neo.Compiler.CSharp/SecurityAnalyzer/ReEntrancyAnalyzer.cs @@ -36,6 +36,7 @@ public class ReEntrancyVulnerabilityPair public readonly Dictionary> callOtherContractInstructions; public readonly Dictionary> writeStorageInstructions; public JToken? DebugInfo { get; init; } + public ReEntrancyVulnerabilityPair( Dictionary> vulnerabilityPairs, Dictionary> callOtherContractInstructions, diff --git a/src/Neo.Compiler.CSharp/SecurityAnalyzer/WriteInTryAnalyzer.cs b/src/Neo.Compiler.CSharp/SecurityAnalyzer/WriteInTryAnalyzer.cs index 7f75d46d5..61ff3ec04 100644 --- a/src/Neo.Compiler.CSharp/SecurityAnalyzer/WriteInTryAnalyzer.cs +++ b/src/Neo.Compiler.CSharp/SecurityAnalyzer/WriteInTryAnalyzer.cs @@ -28,6 +28,7 @@ public class WriteInTryVulnerability // key block writes storage; value blocks in try public readonly Dictionary> vulnerabilities; public JToken? DebugInfo { get; init; } + public WriteInTryVulnerability(Dictionary> vulnerabilities, JToken? debugInfo = null) { this.vulnerabilities = vulnerabilities;