// Copyright (C) 2015-2026 The Neo Project.
//
// NativeContract.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.Cryptography.ECC;
using Neo.SmartContract.Manifest;
using Neo.VM;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace Neo.SmartContract.Native
{
///
/// The base class of all native contracts.
///
public abstract class NativeContract
{
private class NativeContractsCache
{
public record CacheEntry(Dictionary Methods, byte[] Script);
internal Dictionary NativeContracts { get; set; } = new();
public CacheEntry GetAllowedMethods(NativeContract native, ApplicationEngine engine)
{
if (NativeContracts.TryGetValue(native.Id, out var value)) return value;
uint index = engine.PersistingBlock is null ? Ledger.CurrentIndex(engine.SnapshotCache) : engine.PersistingBlock.Index;
CacheEntry methods = native.GetAllowedMethods(engine.ProtocolSettings.IsHardforkEnabled, index);
NativeContracts[native.Id] = methods;
return methods;
}
}
public delegate bool IsHardforkEnabledDelegate(Hardfork hf, uint blockHeight);
private static readonly List s_contractsList = [];
private static readonly Dictionary s_contractsDictionary = new();
private readonly ImmutableHashSet _usedHardforks;
private readonly ReadOnlyCollection _methodDescriptors;
private readonly ReadOnlyCollection _eventsDescriptors;
private static int idCounter = 0;
#region Named Native Contracts
///
/// Gets the instance of the class.
///
public static ContractManagement ContractManagement { get; } = new();
///
/// Gets the instance of the class.
///
public static StdLib StdLib { get; } = new();
///
/// Gets the instance of the class.
///
public static CryptoLib CryptoLib { get; } = new();
///
/// Gets the instance of the class.
///
public static LedgerContract Ledger { get; } = new();
///
/// Gets the instance of the class.
///
public static NeoToken NEO { get; } = new();
///
/// Gets the instance of the class.
///
public static GasToken GAS { get; } = new();
///
/// Gets the instance of the class.
///
public static PolicyContract Policy { get; } = new();
///
/// Gets the instance of the class.
///
public static RoleManagement RoleManagement { get; } = new();
///
/// Gets the instance of the class.
///
public static OracleContract Oracle { get; } = new();
///
/// Gets the instance of the class.
///
public static Notary Notary { get; } = new();
///
/// Gets the instance of the class.
///
public static Treasury Treasury { get; } = new();
///
/// Gets the instance of the class.
///
public static TemporaryStorage TemporaryStorage { get; } = new();
#endregion
///
/// Gets all native contracts.
///
public static IReadOnlyCollection Contracts { get; } = s_contractsList;
///
/// The name of the native contract.
///
public string Name => GetType().Name;
///
/// Since Hardfork has to start having access to the native contract.
///
public Hardfork? ActiveIn => Activations.FirstOrDefault();
///
/// The set of hardforks that contract should be updated at, except the ActiveIn
/// the first entry is the hardfork when the contract will be activated.
///
public virtual ImmutableHashSet Activations { get; } = [];
///
/// The hash of the native contract.
///
public UInt160 Hash { get; }
///
/// The id of the native contract.
///
public int Id { get; } = --idCounter;
///
/// Initializes a new instance of the class.
///
protected NativeContract()
{
Hash = Helper.GetContractHash(UInt160.Zero, 0, Name);
// Reflection to get the methods
List listMethods = [];
foreach (var member in GetType().GetMembers(BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public))
{
foreach (var attribute in member.GetCustomAttributes())
{
listMethods.Add(new ContractMethodMetadata(member, attribute));
}
}
_methodDescriptors = listMethods.OrderBy(p => p.Name, StringComparer.Ordinal).ThenBy(p => p.Parameters.Length).ToList().AsReadOnly();
// Reflection to get the events
_eventsDescriptors =
GetType().GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, null, Array.Empty(), null)!.
GetCustomAttributes().
// Take into account not only the contract constructor, but also the base type constructor for proper FungibleToken events handling.
Concat(GetType().BaseType?.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, null, Array.Empty(), null)!.
GetCustomAttributes() ?? []).
OrderBy(p => p.Order).ToList().AsReadOnly();
// Calculate the initializations forks
_usedHardforks =
_methodDescriptors.Select(u => u.ActiveIn)
.Concat(_methodDescriptors.Select(u => u.DeprecatedIn))
.Concat(_eventsDescriptors.Select(u => u.DeprecatedIn))
.Concat(_eventsDescriptors.Select(u => u.ActiveIn))
.Concat(Activations)
.Where(u => u.HasValue)
.Select(u => u!.Value)
.OrderBy(u => (byte)u)
.Cast().ToImmutableHashSet();
s_contractsList.Add(this);
s_contractsDictionary.Add(Hash, this);
}
///
/// The allowed methods and his offsets.
///
/// Hardfork checker
/// Block height. Used to check the hardforks and active methods.
/// The .
private NativeContractsCache.CacheEntry GetAllowedMethods(IsHardforkEnabledDelegate hfChecker, uint blockHeight)
{
Dictionary methods = new();
// Reflection to get the ContractMethods
byte[] script;
using (ScriptBuilder sb = new())
{
foreach (ContractMethodMetadata method in _methodDescriptors.Where(u => IsActive(u, hfChecker, blockHeight)))
{
method.Descriptor.Offset = sb.Length;
sb.EmitPush(0); //version
methods.Add(sb.Length, method);
sb.EmitSysCall(ApplicationEngine.System_Contract_CallNative);
sb.Emit(OpCode.RET);
}
script = sb.ToArray();
}
return new NativeContractsCache.CacheEntry(methods, script);
}
///
/// The of the native contract.
///
/// The where the HardForks are configured.
/// Block index
/// The .
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ContractState GetContractState(ProtocolSettings settings, uint blockHeight) => GetContractState(settings.IsHardforkEnabled, blockHeight);
internal static bool IsActive(IHardforkActivable u, IsHardforkEnabledDelegate hfChecker, uint blockHeight)
{
// Method/event is active iff ActiveIn hardfork IS active AND DeprecatedIn hardfork IS NOT active.
return (u.ActiveIn is null || hfChecker(u.ActiveIn.Value, blockHeight)) && (u.DeprecatedIn is null || !hfChecker(u.DeprecatedIn.Value, blockHeight));
}
///
/// The of the native contract.
///
/// Hardfork checker
/// Block height. Used to check hardforks and active methods.
/// The .
public ContractState GetContractState(IsHardforkEnabledDelegate hfChecker, uint blockHeight)
{
// Get allowed methods and nef script
var allowedMethods = GetAllowedMethods(hfChecker, blockHeight);
// Compose nef file
var nef = new NefFile()
{
Compiler = "neo-core-v3.0",
Source = string.Empty,
Tokens = [],
Script = allowedMethods.Script
};
nef.CheckSum = NefFile.ComputeChecksum(nef);
// Compose manifest
var manifest = new ContractManifest()
{
Name = Name,
Groups = [],
SupportedStandards = [],
Abi = new ContractAbi
{
Events = _eventsDescriptors
.Where(u => IsActive(u, hfChecker, blockHeight))
.Select(p => p.Descriptor).ToArray(),
Methods = allowedMethods.Methods.Values
.Select(p => p.Descriptor).ToArray()
},
Permissions = [ContractPermission.DefaultPermission],
Trusts = WildcardContainer.Create(),
Extra = null
};
OnManifestCompose(hfChecker, blockHeight, manifest);
// Return ContractState
return new ContractState
{
Id = Id,
Nef = nef,
Hash = Hash,
Manifest = manifest
};
}
protected virtual void OnManifestCompose(IsHardforkEnabledDelegate hfChecker, uint blockHeight, ContractManifest manifest) { }
///
/// It is the initialize block
///
/// The where the HardForks are configured.
/// Block index
/// Active hardforks
/// True if the native contract must be initialized
internal bool IsInitializeBlock(ProtocolSettings settings, uint index, [NotNullWhen(true)] out Hardfork[]? hardforks)
{
var hfs = new List();
// If is in the hardfork height, add them to return array
foreach (var hf in _usedHardforks)
{
if (!settings.Hardforks.TryGetValue(hf, out var activeIn))
{
// If hf is not set in the configuration (with EnsureOmmitedHardforks applied over it), it is treated as disabled.
continue;
}
if (activeIn == index)
{
hfs.Add(hf);
}
}
// Return all initialize hardforks
if (hfs.Count > 0)
{
hardforks = hfs.ToArray();
return true;
}
// If is not configured, the Genesis is an initialization block.
if (index == 0 && ActiveIn is null)
{
hardforks = hfs.ToArray();
return true;
}
// Initialized not required
hardforks = null;
return false;
}
///
/// Is the native contract active
///
/// The where the HardForks are configured.
/// Block height
/// True if the native contract is active
public bool IsActive(ProtocolSettings settings, uint blockHeight)
{
if (ActiveIn is null) return true;
if (!settings.Hardforks.TryGetValue(ActiveIn.Value, out var activeIn))
{
// If is not set in the configuration is treated as enabled from the genesis
activeIn = 0;
}
return activeIn <= blockHeight;
}
///
/// Checks whether the committee has witnessed the current transaction.
///
/// The that is executing the contract.
/// if the committee has witnessed the current transaction; otherwise, .
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected static bool CheckCommittee(ApplicationEngine engine)
{
var committeeMultiSigAddr = NEO.GetCommitteeAddress(engine.SnapshotCache);
return engine.CheckWitnessInternal(committeeMultiSigAddr);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected static void AssertCommittee(ApplicationEngine engine)
{
if (!CheckCommittee(engine))
throw new InvalidOperationException("Invalid committee signature. It should be a multisig(len(committee) - (len(committee) - 1) / 2)).");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected static UInt160 AssertAlmostFullCommittee(ApplicationEngine engine)
{
// Signed by maximum of (half committee + 1) and (committee - 2)
UInt160 committeeMultiSigAddr;
var committees = NativeContract.NEO.GetCommittee(engine.SnapshotCache);
// Min must be almost the committee address
var min = Math.Max(1, committees.Length - (committees.Length - 1) / 2);
committeeMultiSigAddr = Contract.CreateMultiSigRedeemScript(Math.Max(min, committees.Length - 2), committees).ToScriptHash();
if (!engine.CheckWitnessInternal(committeeMultiSigAddr))
throw new InvalidOperationException("Invalid committee signature. It should be a multisig(max(1,len(committee) - 2))).");
return committeeMultiSigAddr;
}
#region Storage keys
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private protected StorageKey CreateStorageKey(byte prefix) => StorageKey.Create(Id, prefix);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private protected StorageKey CreateStorageKey(byte prefix, byte data) => StorageKey.Create(Id, prefix, data);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private protected StorageKey CreateStorageKey(byte prefix, int bigEndianKey) => StorageKey.Create(Id, prefix, bigEndianKey);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private protected StorageKey CreateStorageKey(byte prefix, uint bigEndianKey) => StorageKey.Create(Id, prefix, bigEndianKey);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private protected StorageKey CreateStorageKey(byte prefix, long bigEndianKey) => StorageKey.Create(Id, prefix, bigEndianKey);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private protected StorageKey CreateStorageKey(byte prefix, ulong bigEndianKey) => StorageKey.Create(Id, prefix, bigEndianKey);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private protected StorageKey CreateStorageKey(byte prefix, ReadOnlySpan content) => StorageKey.Create(Id, prefix, content);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private protected StorageKey CreateStorageKey(byte prefix, UInt160 hash) => StorageKey.Create(Id, prefix, hash);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private protected StorageKey CreateStorageKey(byte prefix, UInt256 hash) => StorageKey.Create(Id, prefix, hash);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private protected StorageKey CreateStorageKey(byte prefix, ECPoint pubKey) => StorageKey.Create(Id, prefix, pubKey);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private protected StorageKey CreateStorageKey(byte prefix, UInt256 hash, UInt160 signer) => StorageKey.Create(Id, prefix, hash, signer);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private protected StorageKey CreateStorageKey(byte prefix, UInt160 hash, int bigEndian) => StorageKey.Create(Id, prefix, hash, bigEndian);
#endregion
///
/// Gets the native contract with the specified hash.
///
/// The hash of the native contract.
/// The native contract with the specified hash.
public static NativeContract? GetContract(UInt160 hash)
{
s_contractsDictionary.TryGetValue(hash, out var contract);
return contract;
}
internal Dictionary GetContractMethods(ApplicationEngine engine)
{
var nativeContracts = engine.GetState(() => new NativeContractsCache());
var currentAllowedMethods = nativeContracts.GetAllowedMethods(this, engine);
return currentAllowedMethods.Methods;
}
internal async void Invoke(ApplicationEngine engine, byte version)
{
try
{
if (version != 0)
throw new InvalidOperationException($"The native contract of version {version} is not active.");
// Get native contracts invocation cache
var currentAllowedMethods = GetContractMethods(engine);
// Check if the method is allowed
var context = engine.CurrentContext!;
var method = currentAllowedMethods[context.InstructionPointer];
if (method.ActiveIn is not null && !engine.IsHardforkEnabled(method.ActiveIn.Value))
throw new InvalidOperationException($"Cannot call this method before hardfork {method.ActiveIn}.");
if (method.DeprecatedIn is not null && engine.IsHardforkEnabled(method.DeprecatedIn.Value))
throw new InvalidOperationException($"Cannot call this method after hardfork {method.DeprecatedIn}.");
var state = context.GetState();
if (!state.CallFlags.HasFlag(method.RequiredCallFlags))
throw new InvalidOperationException($"Cannot call this method with the flag {state.CallFlags}.");
// Check native-whitelist
if (!engine.IsHardforkEnabled(Hardfork.HF_Faun) ||
!Policy.IsWhitelistFeeContract(engine.SnapshotCache, Hash, method.Descriptor, out var fixedFee))
{
// In the unit of picoGAS, 1 picoGAS = 1e-12 GAS
engine.AddFee(
(method.CpuFee * engine.ExecFeePicoFactor) +
(method.StorageFee * engine.StoragePrice * ApplicationEngine.FeeFactor), false);
}
List