Skip to content
16 changes: 16 additions & 0 deletions src/Neo/SmartContract/ApplicationEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,14 @@ protected static void OnSysCall(ExecutionEngine engine, Instruction instruction)
/// <param name="datoshi">The amount of GAS, in the unit of datoshi, 1 datoshi = 1e-8 GAS, to be added.</param>
protected internal void AddFee(long datoshi)
{
// Check whitelist

if (CurrentContext?.GetState<ExecutionContextState>()?.WhiteListed == true)
{
// The execution is whitelisted
return;
}

FeeConsumed = checked(FeeConsumed + datoshi);
if (FeeConsumed > _feeAmount)
throw new InvalidOperationException("Insufficient GAS.");
Expand Down Expand Up @@ -319,6 +327,14 @@ private ExecutionContext CallContractInternal(ContractState contract, ContractMe
throw new InvalidOperationException($"Cannot Call Method {method.Name} Of Contract {contract.Hash} From Contract {CurrentScriptHash}");
}

// Check whitelist

if (NativeContract.Policy.IsWhitelistFeeContract(SnapshotCache, contract.Hash, method, out var fixedFee))
{
AddFee(fixedFee.Value);
state.WhiteListed = true;
}

if (invocationCounter.TryGetValue(contract.Hash, out var counter))
{
invocationCounter[contract.Hash] = counter + 1;
Expand Down
5 changes: 5 additions & 0 deletions src/Neo/SmartContract/ExecutionContextState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,9 @@ public class ExecutionContextState
public int NotificationCount { get; set; }

public bool IsDynamicCall { get; set; }

/// <summary>
/// True if the execution is whitelisted by committee
/// </summary>
public bool WhiteListed { get; set; }
}
7 changes: 6 additions & 1 deletion src/Neo/SmartContract/Native/ContractManagement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,8 @@ private ContractTask Update(ApplicationEngine engine, byte[]? nefFile, byte[]? m
// Update nef
contract.Nef = nefFile.AsSerializable<NefFile>();
}
// Clean whitelist (emit event if exists with the old manifest information)
Policy.CleanWhitelist(engine, contract);
if (manifest != null)
{
if (manifest.Length == 0)
Expand All @@ -353,7 +355,8 @@ private ContractTask Update(ApplicationEngine engine, byte[]? nefFile, byte[]? m
contract.Manifest = manifestNew;
}
Helper.Check(new Script(contract.Nef.Script, true), contract.Manifest.Abi);
contract.UpdateCounter++; // Increase update counter
// Increase update counter
contract.UpdateCounter++;
return OnDeployAsync(engine, contract, data, true);
}

Expand All @@ -374,6 +377,8 @@ private async ContractTask Destroy(ApplicationEngine engine)
engine.SnapshotCache.Delete(key);
// lock contract
await Policy.BlockAccountInternal(engine, hash);
// Clean whitelist (emit event if exists with the old manifest information)
Policy.CleanWhitelist(engine, contract);
// emit event
Notify(engine, "Destroy", hash);
}
Expand Down
12 changes: 10 additions & 2 deletions src/Neo/SmartContract/Native/NativeContract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,10 @@ private protected StorageKey CreateStorageKey(byte prefix, params IEnumerable<IS
return builder;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private protected StorageKey CreateStorageKey(byte prefix, UInt160 hash, int bigEndianKey)
=> new KeyBuilder(Id, prefix) { hash, bigEndianKey };

#endregion

/// <summary>
Expand Down Expand Up @@ -425,8 +429,12 @@ internal async void Invoke(ApplicationEngine engine, byte version)
var state = context.GetState<ExecutionContextState>();
if (!state.CallFlags.HasFlag(method.RequiredCallFlags))
throw new InvalidOperationException($"Cannot call this method with the flag {state.CallFlags}.");
// In the unit of datoshi, 1 datoshi = 1e-8 GAS
engine.AddFee(method.CpuFee * engine.ExecFeeFactor + method.StorageFee * engine.StoragePrice);
// Check native-whitelist
if (!Policy.IsWhitelistFeeContract(engine.SnapshotCache, Hash, method.Descriptor, out var fixedFee))
{
// In the unit of datoshi, 1 datoshi = 1e-8 GAS
engine.AddFee(method.CpuFee * engine.ExecFeeFactor + method.StorageFee * engine.StoragePrice);
}
List<object?> parameters = new();
if (method.NeedApplicationEngine) parameters.Add(engine);
if (method.NeedSnapshot) parameters.Add(engine.SnapshotCache);
Expand Down
139 changes: 138 additions & 1 deletion src/Neo/SmartContract/Native/PolicyContract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,22 @@
using Neo.Network.P2P.Payloads;
using Neo.Persistence;
using Neo.SmartContract.Iterators;
using Neo.SmartContract.Manifest;
using System.Buffers.Binary;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;

namespace Neo.SmartContract.Native;

/// <summary>
/// A native contract that manages the system policies.
/// </summary>
[ContractEvent(0, name: WhitelistChangedEventName,
"contract", ContractParameterType.Hash160,
"method", ContractParameterType.String,
"argCount", ContractParameterType.Integer,
"fee", ContractParameterType.Any
)]
public sealed class PolicyContract : NativeContract
{
/// <summary>
Expand Down Expand Up @@ -64,8 +73,9 @@ public sealed class PolicyContract : NativeContract
/// </summary>
public const uint MaxStoragePrice = 10000000;

private const byte Prefix_BlockedAccount = 15;
private const byte Prefix_FeePerByte = 10;
private const byte Prefix_BlockedAccount = 15;
private const byte Prefix_WhitelistedFeeContracts = 16;
private const byte Prefix_ExecFeeFactor = 18;
private const byte Prefix_StoragePrice = 19;
private const byte Prefix_AttributeFee = 20;
Expand All @@ -74,6 +84,8 @@ public sealed class PolicyContract : NativeContract
private readonly StorageKey _execFeeFactor;
private readonly StorageKey _storagePrice;

private const string WhitelistChangedEventName = "WhitelistFeeChanged";

internal PolicyContract()
{
_feePerByte = CreateStorageKey(Prefix_FeePerByte);
Expand Down Expand Up @@ -237,6 +249,131 @@ private bool UnblockAccount(ApplicationEngine engine, UInt160 account)
return true;
}

internal bool IsWhitelistFeeContract(DataCache snapshot, UInt160 contractHash, ContractMethodDescriptor method, [NotNullWhen(true)] out long? fixedFee)
{
// Check contract existence

var currentContract = ContractManagement.GetContract(snapshot, contractHash);

if (currentContract != null)
{
// Check state existence

var item = snapshot.TryGet(CreateStorageKey(Prefix_WhitelistedFeeContracts, contractHash, method.Offset));

if (item != null)
{
fixedFee = (long)(BigInteger)item;
return true;
}
}

fixedFee = null;
return false;
}

/// <summary>
/// Remove whitelisted Fee contracts
/// </summary>
/// <param name="engine">The execution engine.</param>
/// <param name="contractHash">The contract to set the whitelist</param>
/// <param name="method">Method</param>
/// <param name="argCount">Argument count</param>
[ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)]
private void RemoveWhitelistFeeContract(ApplicationEngine engine, UInt160 contractHash, string method, int argCount)
{
if (!CheckCommittee(engine)) throw new InvalidOperationException("Invalid committee signature");

// Validate methods
var contract = ContractManagement.GetContract(engine.SnapshotCache, contractHash)
?? throw new InvalidOperationException("Is not a valid contract");

// If exists multiple instance a exception is throwed
var methodDescriptor = contract.Manifest.Abi.Methods.SingleOrDefault(u => u.Name == method && u.Parameters.Length == argCount) ??
throw new InvalidOperationException($"Method {method} with {argCount} args was not found in {contractHash}");
var key = CreateStorageKey(Prefix_WhitelistedFeeContracts, contractHash, methodDescriptor.Offset);

if (!engine.SnapshotCache.Contains(key)) throw new InvalidOperationException("Whitelist not found");

engine.SnapshotCache.Delete(key);

// Emit event
Notify(engine, WhitelistChangedEventName, contractHash, method, argCount, null);
}

internal int CleanWhitelist(ApplicationEngine engine, ContractState contract)
{
var count = 0;
var searchKey = CreateStorageKey(Prefix_WhitelistedFeeContracts, contract.Hash);

foreach ((var key, _) in engine.SnapshotCache.Find(searchKey, SeekDirection.Forward))
{
engine.SnapshotCache.Delete(key);
count++;

// Emit event recovering the values from the Key

var keyData = key.ToArray().AsSpan();
var methodOffset = BinaryPrimitives.ReadInt32BigEndian(keyData.Slice(sizeof(int) + sizeof(byte) + UInt160.Length, sizeof(int)));

// Get method for event
var method = contract.Manifest.Abi.Methods.FirstOrDefault(m => m.Offset == methodOffset);

Notify(engine, WhitelistChangedEventName, contract.Hash, method?.Name, method?.Parameters.Length, null);
}

return count;
}

/// <summary>
/// Set whitelisted Fee contracts
/// </summary>
/// <param name="engine">The execution engine.</param>
/// <param name="contractHash">The contract to set the whitelist</param>
/// <param name="method">Method</param>
/// <param name="argCount">Argument count</param>
/// <param name="fixedFee">Fixed execution fee</param>
[ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)]
internal void SetWhitelistFeeContract(ApplicationEngine engine, UInt160 contractHash, string method, int argCount, long fixedFee)
{
ArgumentOutOfRangeException.ThrowIfNegative(fixedFee, nameof(fixedFee));

if (!CheckCommittee(engine)) throw new InvalidOperationException("Invalid committee signature");

// Validate methods
var contract = ContractManagement.GetContract(engine.SnapshotCache, contractHash)
?? throw new InvalidOperationException("Is not a valid contract");

if (contract.Manifest.Abi.GetMethod(method, argCount) is null)
throw new InvalidOperationException($"{method} with {argCount} args is not a valid method of {contractHash}");

// If exists multiple instance a exception is throwed
var methodDescriptor = contract.Manifest.Abi.Methods.SingleOrDefault(u => u.Name == method && u.Parameters.Length == argCount) ??
throw new InvalidOperationException($"Method {method} with {argCount} args was not found in {contractHash}");
var key = CreateStorageKey(Prefix_WhitelistedFeeContracts, contractHash, methodDescriptor.Offset);

// Set
var entry = engine.SnapshotCache
.GetAndChange(key, () => new StorageItem(fixedFee));

entry.Set(fixedFee);

// Emit event

Notify(engine, WhitelistChangedEventName, contractHash, method, argCount, fixedFee);
}

[ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)]
internal StorageIterator GetWhitelistFeeContracts(DataCache snapshot)
{
const FindOptions options = FindOptions.RemovePrefix | FindOptions.KeysOnly;
var enumerator = snapshot
.Find(CreateStorageKey(Prefix_WhitelistedFeeContracts), SeekDirection.Forward)
.GetEnumerator();

return new StorageIterator(enumerator, 1, options);
}

[ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)]
private StorageIterator GetBlockedAccounts(DataCache snapshot)
{
Expand Down
Loading