diff --git a/src/Neo/SmartContract/ApplicationEngine.cs b/src/Neo/SmartContract/ApplicationEngine.cs
index e3b10b746d..87eda1774b 100644
--- a/src/Neo/SmartContract/ApplicationEngine.cs
+++ b/src/Neo/SmartContract/ApplicationEngine.cs
@@ -300,6 +300,14 @@ protected static void OnSysCall(ExecutionEngine engine, Instruction instruction)
/// The amount of GAS, in the unit of datoshi, 1 datoshi = 1e-8 GAS, to be added.
protected internal void AddFee(long datoshi)
{
+ // Check whitelist
+
+ if (CurrentContext?.GetState()?.WhiteListed == true)
+ {
+ // The execution is whitelisted
+ return;
+ }
+
#pragma warning disable CS0618 // Type or member is obsolete
FeeConsumed = GasConsumed = checked(FeeConsumed + datoshi);
#pragma warning restore CS0618 // Type or member is obsolete
@@ -342,12 +350,21 @@ private ExecutionContext CallContractInternal(ContractState contract, ContractMe
else
{
var executingContract = IsHardforkEnabled(Hardfork.HF_Domovoi)
- ? state.Contract // use executing contract state to avoid possible contract update/destroy side-effects, ref. https://github.com/neo-project/neo/pull/3290.
- : NativeContract.ContractManagement.GetContract(SnapshotCache, CurrentScriptHash!);
+ ? state.Contract // use executing contract state to avoid possible contract update/destroy side-effects, ref. https://github.com/neo-project/neo/pull/3290.
+ : NativeContract.ContractManagement.GetContract(SnapshotCache, CurrentScriptHash!);
if (executingContract?.CanCall(contract, method.Name) == false)
throw new InvalidOperationException($"Cannot Call Method {method.Name} Of Contract {contract.Hash} From Contract {CurrentScriptHash}");
}
+ // Check whitelist
+
+ if (IsHardforkEnabled(Hardfork.HF_Faun) &&
+ NativeContract.Policy.IsWhitelistFeeContract(SnapshotCache, contract.Hash, method.Name, method.Parameters.Length, out var fixedFee))
+ {
+ AddFee(fixedFee.Value);
+ state.WhiteListed = true;
+ }
+
if (invocationCounter.TryGetValue(contract.Hash, out var counter))
{
invocationCounter[contract.Hash] = counter + 1;
diff --git a/src/Neo/SmartContract/ExecutionContextState.cs b/src/Neo/SmartContract/ExecutionContextState.cs
index e5b10adf32..44be42e2b7 100644
--- a/src/Neo/SmartContract/ExecutionContextState.cs
+++ b/src/Neo/SmartContract/ExecutionContextState.cs
@@ -53,5 +53,10 @@ public class ExecutionContextState
public int NotificationCount { get; set; }
public bool IsDynamicCall { get; set; }
+
+ ///
+ /// True if the execution is whitelisted by committee
+ ///
+ public bool WhiteListed { get; set; } = false;
}
}
diff --git a/src/Neo/SmartContract/Manifest/ContractAbi.cs b/src/Neo/SmartContract/Manifest/ContractAbi.cs
index 054a972e56..998efe92bb 100644
--- a/src/Neo/SmartContract/Manifest/ContractAbi.cs
+++ b/src/Neo/SmartContract/Manifest/ContractAbi.cs
@@ -88,8 +88,10 @@ public static ContractAbi FromJson(JObject json)
if (pcount >= 0)
{
methodDictionary ??= Methods.ToDictionary(p => (p.Name, p.Parameters.Length));
- methodDictionary.TryGetValue((name, pcount), out var method);
- return method;
+ if (methodDictionary.TryGetValue((name, pcount), out var method))
+ return method;
+
+ return null;
}
else
{
diff --git a/src/Neo/SmartContract/Native/ContractManagement.cs b/src/Neo/SmartContract/Native/ContractManagement.cs
index d71771189e..f00eadc215 100644
--- a/src/Neo/SmartContract/Native/ContractManagement.cs
+++ b/src/Neo/SmartContract/Native/ContractManagement.cs
@@ -369,6 +369,11 @@ private ContractTask Update(ApplicationEngine engine, byte[] nefFile, byte[] man
}
Helper.Check(new Script(contract.Nef.Script, engine.IsHardforkEnabled(Hardfork.HF_Basilisk)), contract.Manifest.Abi);
contract.UpdateCounter++; // Increase update counter
+
+ // Clean whitelist (emit event if exists)
+
+ Policy.CleanWhitelist(engine, contract.Hash);
+
return OnDeployAsync(engine, contract, data, true);
}
diff --git a/src/Neo/SmartContract/Native/NativeContract.cs b/src/Neo/SmartContract/Native/NativeContract.cs
index dc514468a4..bda8cdc4c6 100644
--- a/src/Neo/SmartContract/Native/NativeContract.cs
+++ b/src/Neo/SmartContract/Native/NativeContract.cs
@@ -394,6 +394,9 @@ protected static void AssertCommittee(ApplicationEngine engine)
[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, string methodName, int bigEndianKey) => StorageKey.Create(Id, prefix, hash, methodName, bigEndianKey);
+
#endregion
///
@@ -432,8 +435,13 @@ internal async void Invoke(ApplicationEngine engine, byte version)
var state = context.GetState();
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 (!engine.IsHardforkEnabled(Hardfork.HF_Faun) ||
+ !Policy.IsWhitelistFeeContract(engine.SnapshotCache, Hash, method.Name, method.Parameters.Length, out var fixedFee))
+ {
+ // In the unit of datoshi, 1 datoshi = 1e-8 GAS
+ engine.AddFee(method.CpuFee * engine.ExecFeeFactor + method.StorageFee * engine.StoragePrice);
+ }
List
private const string MillisecondsPerBlockChangedEventName = "MillisecondsPerBlockChanged";
+ private const string WhitelistChangedEventName = "WhitelistChanged";
+
[ContractEvent(Hardfork.HF_Echidna, 0, name: MillisecondsPerBlockChangedEventName,
"old", ContractParameterType.Integer,
"new", ContractParameterType.Integer
)]
+ [ContractEvent(Hardfork.HF_Faun, 1, name: WhitelistChangedEventName,
+ "contract", ContractParameterType.Hash160,
+ "method", ContractParameterType.String,
+ "argCount", ContractParameterType.Integer,
+ "fee", ContractParameterType.Any
+ )]
internal PolicyContract() : base()
{
_feePerByte = CreateStorageKey(Prefix_FeePerByte);
@@ -258,6 +270,113 @@ public bool IsBlocked(IReadOnlyStore snapshot, UInt160 account)
return snapshot.Contains(CreateStorageKey(Prefix_BlockedAccount, account));
}
+ internal bool IsWhitelistFeeContract(DataCache snapshot, UInt160 contractHash, string method, int argCount, [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, argCount));
+
+ if (item != null)
+ {
+ fixedFee = (long)(BigInteger)item;
+ return true;
+ }
+ }
+
+ fixedFee = null;
+ return false;
+ }
+
+ ///
+ /// Remove whitelisted Fee contracts
+ ///
+ /// The execution engine.
+ /// The contract to set the whitelist
+ /// Method
+ /// Argument count
+ [ContractMethod(Hardfork.HF_Faun, 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");
+
+ var key = CreateStorageKey(Prefix_WhitelistedFeeContracts, contractHash, method, argCount);
+
+ if (!engine.SnapshotCache.Contains(key)) throw new InvalidOperationException("Whitelist not found");
+
+ engine.SnapshotCache.Delete(key);
+
+ // Emit event
+ engine.SendNotification(Hash, WhitelistChangedEventName,
+ [new VM.Types.ByteString(contractHash.ToArray()), new VM.Types.ByteString(method.ToStrictUtf8Bytes()),
+ new VM.Types.Integer(argCount), VM.Types.StackItem.Null]);
+ }
+
+ internal int CleanWhitelist(ApplicationEngine engine, UInt160 contractHash)
+ {
+ var count = 0;
+ var searchKey = CreateStorageKey(Prefix_WhitelistedFeeContracts, contractHash);
+
+ 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 method, var argCount) = StorageKey.ReadMethodAndArgCount(key.ToArray().AsSpan());
+
+ engine.SendNotification(Hash, WhitelistChangedEventName,
+ [new VM.Types.ByteString(contractHash.ToArray()), new VM.Types.ByteString(method.ToStrictUtf8Bytes()),
+ new VM.Types.Integer(argCount), VM.Types.StackItem.Null]);
+ }
+
+ return count;
+ }
+
+ ///
+ /// Set whitelisted Fee contracts
+ ///
+ /// The execution engine.
+ /// The contract to set the whitelist
+ /// Method
+ /// Argument count
+ /// Fixed execution fee
+ [ContractMethod(Hardfork.HF_Faun, 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");
+
+ var key = CreateStorageKey(Prefix_WhitelistedFeeContracts, contractHash, method, argCount);
+
+ // 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}");
+
+ // Set
+ var entry = engine.SnapshotCache
+ .GetAndChange(key, () => new StorageItem(fixedFee));
+
+ entry.Set(fixedFee);
+
+ // Emit event
+
+ engine.SendNotification(Hash, WhitelistChangedEventName,
+ [new VM.Types.ByteString(contractHash.ToArray()), new VM.Types.ByteString(method.ToStrictUtf8Bytes()),
+ new VM.Types.Integer(argCount), new VM.Types.Integer(fixedFee)]);
+ }
+
///
/// Sets the block generation time in milliseconds.
///
@@ -438,5 +557,16 @@ private StorageIterator GetBlockedAccounts(DataCache snapshot)
.GetEnumerator();
return new StorageIterator(enumerator, 1, options);
}
+
+ [ContractMethod(Hardfork.HF_Faun, 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);
+ }
}
}
diff --git a/src/Neo/SmartContract/StorageKey.cs b/src/Neo/SmartContract/StorageKey.cs
index 5f870558e3..07bafd23be 100644
--- a/src/Neo/SmartContract/StorageKey.cs
+++ b/src/Neo/SmartContract/StorageKey.cs
@@ -181,6 +181,37 @@ public static StorageKey Create(int id, byte prefix, UInt256 hash, UInt160 signe
return new(id, data);
}
+ ///
+ /// Create StorageKey
+ ///
+ /// The id of the contract.
+ /// The prefix of the key.
+ /// Hash
+ /// Method Name
+ /// Big Endian key.
+ /// The class
+ public static StorageKey Create(int id, byte prefix, UInt160 hash, string methodName, int bigEndian)
+ {
+ const int HashAndInt = UInt160Length + sizeof(int);
+
+ var methodData = methodName.ToStrictUtf8Bytes();
+ var data = new byte[HashAndInt + methodData.Length];
+
+ FillHeader(data, id, prefix);
+ hash.Serialize(data.AsSpan(PrefixLength..));
+ BinaryPrimitives.WriteInt32BigEndian(data.AsSpan(UInt160Length..), bigEndian);
+ Array.Copy(methodData, 0, data, HashAndInt, methodData.Length);
+
+ return new(id, data);
+ }
+
+ internal static (string methodName, int bigEndian) ReadMethodAndArgCount(ReadOnlySpan keyData)
+ {
+ var argCount = BinaryPrimitives.ReadInt32BigEndian(keyData.Slice(UInt160Length, 4));
+ var method = keyData[(UInt160Length + 4)..];
+ return (method.ToStrictUtf8String(), argCount);
+ }
+
///
/// Create StorageKey
///
diff --git a/tests/Neo.UnitTests/Ledger/UT_StorageKey.cs b/tests/Neo.UnitTests/Ledger/UT_StorageKey.cs
index 03a9bb447b..eb7ef67a68 100644
--- a/tests/Neo.UnitTests/Ledger/UT_StorageKey.cs
+++ b/tests/Neo.UnitTests/Ledger/UT_StorageKey.cs
@@ -14,6 +14,7 @@
using Neo.Extensions;
using Neo.SmartContract;
using System;
+using System.Text;
namespace Neo.UnitTests.Ledger
{
@@ -76,6 +77,23 @@ public void SameTest()
UInt256.Parse("0x761a9bb72ca2a63984db0cc43f943a2a25e464f62d1a91114c2b6fbbfd24b51d"),
UInt160.Parse("2d3b96ae1bcc5a585e075e3b81920210dec16302")).ToArray());
+ // UInt160+String+Int
+ key = new KeyBuilder(1, 2);
+ key.Add(UInt160.Parse("2d3b96ae1bcc5a585e075e3b81920210dec16302"));
+ key.AddBigEndian((int)3); // arg count
+ key.Add(Encoding.UTF8.GetBytes("hello world"));
+
+ CollectionAssert.AreEqual(key.ToArray(), StorageKey.Create(1, 2,
+ UInt160.Parse("2d3b96ae1bcc5a585e075e3b81920210dec16302"), "hello world", 3).ToArray());
+
+ // Recover method and arg count
+
+ var keyB = new StorageKey(key.ToArray()).ToArray().AsSpan();
+ (var method, var argCount) = StorageKey.ReadMethodAndArgCount(keyB);
+
+ Assert.AreEqual(3, argCount);
+ Assert.AreEqual("hello world", method);
+
// ISerializable
key = new KeyBuilder(1, 2);
key.Add(ECCurve.Secp256r1.G);
diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs
index 03f58a8dbd..0fa96485ca 100644
--- a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs
+++ b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs
@@ -48,7 +48,7 @@ public void TestSetup()
{"LedgerContract", """{"id":-4,"updatecounter":0,"hash":"0xda65b600f7124ce6c79950c1772a36403104f2be","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":1110259869},"manifest":{"name":"LedgerContract","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"currentHash","parameters":[],"returntype":"Hash256","offset":0,"safe":true},{"name":"currentIndex","parameters":[],"returntype":"Integer","offset":7,"safe":true},{"name":"getBlock","parameters":[{"name":"indexOrHash","type":"ByteArray"}],"returntype":"Array","offset":14,"safe":true},{"name":"getTransaction","parameters":[{"name":"hash","type":"Hash256"}],"returntype":"Array","offset":21,"safe":true},{"name":"getTransactionFromBlock","parameters":[{"name":"blockIndexOrHash","type":"ByteArray"},{"name":"txIndex","type":"Integer"}],"returntype":"Array","offset":28,"safe":true},{"name":"getTransactionHeight","parameters":[{"name":"hash","type":"Hash256"}],"returntype":"Integer","offset":35,"safe":true},{"name":"getTransactionSigners","parameters":[{"name":"hash","type":"Hash256"}],"returntype":"Array","offset":42,"safe":true},{"name":"getTransactionVMState","parameters":[{"name":"hash","type":"Hash256"}],"returntype":"Integer","offset":49,"safe":true}],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""},
{"NeoToken", """{"id":-5,"updatecounter":0,"hash":"0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dA","checksum":1991619121},"manifest":{"name":"NeoToken","groups":[],"features":{},"supportedstandards":["NEP-17","NEP-27"],"abi":{"methods":[{"name":"balanceOf","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Integer","offset":0,"safe":true},{"name":"decimals","parameters":[],"returntype":"Integer","offset":7,"safe":true},{"name":"getAccountState","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Array","offset":14,"safe":true},{"name":"getAllCandidates","parameters":[],"returntype":"InteropInterface","offset":21,"safe":true},{"name":"getCandidateVote","parameters":[{"name":"pubKey","type":"PublicKey"}],"returntype":"Integer","offset":28,"safe":true},{"name":"getCandidates","parameters":[],"returntype":"Array","offset":35,"safe":true},{"name":"getCommittee","parameters":[],"returntype":"Array","offset":42,"safe":true},{"name":"getCommitteeAddress","parameters":[],"returntype":"Hash160","offset":49,"safe":true},{"name":"getGasPerBlock","parameters":[],"returntype":"Integer","offset":56,"safe":true},{"name":"getNextBlockValidators","parameters":[],"returntype":"Array","offset":63,"safe":true},{"name":"getRegisterPrice","parameters":[],"returntype":"Integer","offset":70,"safe":true},{"name":"onNEP17Payment","parameters":[{"name":"from","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Void","offset":77,"safe":false},{"name":"registerCandidate","parameters":[{"name":"pubkey","type":"PublicKey"}],"returntype":"Boolean","offset":84,"safe":false},{"name":"setGasPerBlock","parameters":[{"name":"gasPerBlock","type":"Integer"}],"returntype":"Void","offset":91,"safe":false},{"name":"setRegisterPrice","parameters":[{"name":"registerPrice","type":"Integer"}],"returntype":"Void","offset":98,"safe":false},{"name":"symbol","parameters":[],"returntype":"String","offset":105,"safe":true},{"name":"totalSupply","parameters":[],"returntype":"Integer","offset":112,"safe":true},{"name":"transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Boolean","offset":119,"safe":false},{"name":"unclaimedGas","parameters":[{"name":"account","type":"Hash160"},{"name":"end","type":"Integer"}],"returntype":"Integer","offset":126,"safe":true},{"name":"unregisterCandidate","parameters":[{"name":"pubkey","type":"PublicKey"}],"returntype":"Boolean","offset":133,"safe":false},{"name":"vote","parameters":[{"name":"account","type":"Hash160"},{"name":"voteTo","type":"PublicKey"}],"returntype":"Boolean","offset":140,"safe":false}],"events":[{"name":"Transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"}]},{"name":"CandidateStateChanged","parameters":[{"name":"pubkey","type":"PublicKey"},{"name":"registered","type":"Boolean"},{"name":"votes","type":"Integer"}]},{"name":"Vote","parameters":[{"name":"account","type":"Hash160"},{"name":"from","type":"PublicKey"},{"name":"to","type":"PublicKey"},{"name":"amount","type":"Integer"}]},{"name":"CommitteeChanged","parameters":[{"name":"old","type":"Array"},{"name":"new","type":"Array"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""},
{"GasToken", """{"id":-6,"updatecounter":0,"hash":"0xd2a4cff31913016155e38e474a2c06d08be276cf","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":2663858513},"manifest":{"name":"GasToken","groups":[],"features":{},"supportedstandards":["NEP-17"],"abi":{"methods":[{"name":"balanceOf","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Integer","offset":0,"safe":true},{"name":"decimals","parameters":[],"returntype":"Integer","offset":7,"safe":true},{"name":"symbol","parameters":[],"returntype":"String","offset":14,"safe":true},{"name":"totalSupply","parameters":[],"returntype":"Integer","offset":21,"safe":true},{"name":"transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Boolean","offset":28,"safe":false}],"events":[{"name":"Transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""},
- {"PolicyContract", """{"id":-7,"updatecounter":0,"hash":"0xcc5e4edd9f5f8dba8bb65734541df7a1c081c67b","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dA","checksum":2208257578},"manifest":{"name":"PolicyContract","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"blockAccount","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":0,"safe":false},{"name":"getAttributeFee","parameters":[{"name":"attributeType","type":"Integer"}],"returntype":"Integer","offset":7,"safe":true},{"name":"getBlockedAccounts","parameters":[],"returntype":"InteropInterface","offset":14,"safe":true},{"name":"getExecFeeFactor","parameters":[],"returntype":"Integer","offset":21,"safe":true},{"name":"getFeePerByte","parameters":[],"returntype":"Integer","offset":28,"safe":true},{"name":"getMaxTraceableBlocks","parameters":[],"returntype":"Integer","offset":35,"safe":true},{"name":"getMaxValidUntilBlockIncrement","parameters":[],"returntype":"Integer","offset":42,"safe":true},{"name":"getMillisecondsPerBlock","parameters":[],"returntype":"Integer","offset":49,"safe":true},{"name":"getStoragePrice","parameters":[],"returntype":"Integer","offset":56,"safe":true},{"name":"isBlocked","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":63,"safe":true},{"name":"setAttributeFee","parameters":[{"name":"attributeType","type":"Integer"},{"name":"value","type":"Integer"}],"returntype":"Void","offset":70,"safe":false},{"name":"setExecFeeFactor","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":77,"safe":false},{"name":"setFeePerByte","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":84,"safe":false},{"name":"setMaxTraceableBlocks","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":91,"safe":false},{"name":"setMaxValidUntilBlockIncrement","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":98,"safe":false},{"name":"setMillisecondsPerBlock","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":105,"safe":false},{"name":"setStoragePrice","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":112,"safe":false},{"name":"unblockAccount","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":119,"safe":false}],"events":[{"name":"MillisecondsPerBlockChanged","parameters":[{"name":"old","type":"Integer"},{"name":"new","type":"Integer"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""},
+ {"PolicyContract", """{"id":-7,"updatecounter":0,"hash":"0xcc5e4edd9f5f8dba8bb65734541df7a1c081c67b","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dA","checksum":1991619121},"manifest":{"name":"PolicyContract","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"blockAccount","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":0,"safe":false},{"name":"getAttributeFee","parameters":[{"name":"attributeType","type":"Integer"}],"returntype":"Integer","offset":7,"safe":true},{"name":"getBlockedAccounts","parameters":[],"returntype":"InteropInterface","offset":14,"safe":true},{"name":"getExecFeeFactor","parameters":[],"returntype":"Integer","offset":21,"safe":true},{"name":"getFeePerByte","parameters":[],"returntype":"Integer","offset":28,"safe":true},{"name":"getMaxTraceableBlocks","parameters":[],"returntype":"Integer","offset":35,"safe":true},{"name":"getMaxValidUntilBlockIncrement","parameters":[],"returntype":"Integer","offset":42,"safe":true},{"name":"getMillisecondsPerBlock","parameters":[],"returntype":"Integer","offset":49,"safe":true},{"name":"getStoragePrice","parameters":[],"returntype":"Integer","offset":56,"safe":true},{"name":"getWhitelistFeeContracts","parameters":[],"returntype":"InteropInterface","offset":63,"safe":true},{"name":"isBlocked","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":70,"safe":true},{"name":"removeWhitelistFeeContract","parameters":[{"name":"contractHash","type":"Hash160"},{"name":"method","type":"String"},{"name":"argCount","type":"Integer"}],"returntype":"Void","offset":77,"safe":false},{"name":"setAttributeFee","parameters":[{"name":"attributeType","type":"Integer"},{"name":"value","type":"Integer"}],"returntype":"Void","offset":84,"safe":false},{"name":"setExecFeeFactor","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":91,"safe":false},{"name":"setFeePerByte","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":98,"safe":false},{"name":"setMaxTraceableBlocks","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":105,"safe":false},{"name":"setMaxValidUntilBlockIncrement","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":112,"safe":false},{"name":"setMillisecondsPerBlock","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":119,"safe":false},{"name":"setStoragePrice","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":126,"safe":false},{"name":"setWhitelistFeeContract","parameters":[{"name":"contractHash","type":"Hash160"},{"name":"method","type":"String"},{"name":"argCount","type":"Integer"},{"name":"fixedFee","type":"Integer"}],"returntype":"Void","offset":133,"safe":false},{"name":"unblockAccount","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":140,"safe":false}],"events":[{"name":"MillisecondsPerBlockChanged","parameters":[{"name":"old","type":"Integer"},{"name":"new","type":"Integer"}]},{"name":"WhitelistChanged","parameters":[{"name":"contract","type":"Hash160"},{"name":"method","type":"String"},{"name":"argCount","type":"Integer"},{"name":"fee","type":"Any"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""},
{"RoleManagement", """{"id":-8,"updatecounter":0,"hash":"0x49cf4e5378ffcd4dec034fd98a174c5491e395e2","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0A=","checksum":983638438},"manifest":{"name":"RoleManagement","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"designateAsRole","parameters":[{"name":"role","type":"Integer"},{"name":"nodes","type":"Array"}],"returntype":"Void","offset":0,"safe":false},{"name":"getDesignatedByRole","parameters":[{"name":"role","type":"Integer"},{"name":"index","type":"Integer"}],"returntype":"Array","offset":7,"safe":true}],"events":[{"name":"Designation","parameters":[{"name":"Role","type":"Integer"},{"name":"BlockIndex","type":"Integer"},{"name":"Old","type":"Array"},{"name":"New","type":"Array"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""},
{"OracleContract", """{"id":-9,"updatecounter":0,"hash":"0xfe924b7cfe89ddd271abaf7210a80a7e11178758","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":2663858513},"manifest":{"name":"OracleContract","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"finish","parameters":[],"returntype":"Void","offset":0,"safe":false},{"name":"getPrice","parameters":[],"returntype":"Integer","offset":7,"safe":true},{"name":"request","parameters":[{"name":"url","type":"String"},{"name":"filter","type":"String"},{"name":"callback","type":"String"},{"name":"userData","type":"Any"},{"name":"gasForResponse","type":"Integer"}],"returntype":"Void","offset":14,"safe":false},{"name":"setPrice","parameters":[{"name":"price","type":"Integer"}],"returntype":"Void","offset":21,"safe":false},{"name":"verify","parameters":[],"returntype":"Boolean","offset":28,"safe":true}],"events":[{"name":"OracleRequest","parameters":[{"name":"Id","type":"Integer"},{"name":"RequestContract","type":"Hash160"},{"name":"Url","type":"String"},{"name":"Filter","type":"String"}]},{"name":"OracleResponse","parameters":[{"name":"Id","type":"Integer"},{"name":"OriginalTx","type":"Hash256"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""},
{"Notary", """{"id":-10,"updatecounter":0,"hash":"0xc1e14f19c3e60d0b9244d06dd7ba9b113135ec3b","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":1110259869},"manifest":{"name":"Notary","groups":[],"features":{},"supportedstandards":["NEP-27"],"abi":{"methods":[{"name":"balanceOf","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Integer","offset":0,"safe":true},{"name":"expirationOf","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Integer","offset":7,"safe":true},{"name":"getMaxNotValidBeforeDelta","parameters":[],"returntype":"Integer","offset":14,"safe":true},{"name":"lockDepositUntil","parameters":[{"name":"account","type":"Hash160"},{"name":"till","type":"Integer"}],"returntype":"Boolean","offset":21,"safe":false},{"name":"onNEP17Payment","parameters":[{"name":"from","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Void","offset":28,"safe":false},{"name":"setMaxNotValidBeforeDelta","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":35,"safe":false},{"name":"verify","parameters":[{"name":"signature","type":"ByteArray"}],"returntype":"Boolean","offset":42,"safe":true},{"name":"withdraw","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"}],"returntype":"Boolean","offset":49,"safe":false}],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}
diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_PolicyContract.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_PolicyContract.cs
index 07dd41df5c..00b4bcd9ed 100644
--- a/tests/Neo.UnitTests/SmartContract/Native/UT_PolicyContract.cs
+++ b/tests/Neo.UnitTests/SmartContract/Native/UT_PolicyContract.cs
@@ -10,11 +10,13 @@
// modifications are permitted.
using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Neo.Cryptography;
using Neo.Extensions;
using Neo.Network.P2P.Payloads;
using Neo.Persistence;
using Neo.SmartContract;
using Neo.SmartContract.Iterators;
+using Neo.SmartContract.Manifest;
using Neo.SmartContract.Native;
using Neo.UnitTests.Extensions;
using Neo.VM;
@@ -22,6 +24,7 @@
using System;
using System.Linq;
using System.Numerics;
+using System.Reflection;
using Boolean = Neo.VM.Types.Boolean;
namespace Neo.UnitTests.SmartContract.Native
@@ -626,5 +629,172 @@ public void TestListBlockedAccounts()
Assert.IsTrue(iter.Next());
Assert.AreEqual(new UInt160(iter.Value(new ReferenceCounter()).GetSpan()), UInt160.Zero);
}
+
+ [TestMethod]
+ public void TestWhiteListFee()
+ {
+ // Create script
+
+ var snapshotCache = _snapshotCache.CloneCache();
+
+ byte[] script;
+ using (var sb = new ScriptBuilder())
+ {
+ sb.EmitDynamicCall(NativeContract.NEO.Hash, "balanceOf", NativeContract.NEO.GetCommitteeAddress(_snapshotCache.CloneCache()));
+ script = sb.ToArray();
+ }
+
+ var engine = CreateEngineWithCommitteeSigner(snapshotCache, script);
+
+ // Not whitelisted
+
+ Assert.AreEqual(VMState.HALT, engine.Execute());
+ Assert.AreEqual(0, engine.ResultStack.Pop().GetInteger());
+ Assert.AreEqual(2028330, engine.FeeConsumed);
+ Assert.AreEqual(0, NativeContract.Policy.CleanWhitelist(engine, NativeContract.NEO.Hash));
+ Assert.IsEmpty(engine.Notifications);
+
+ // Whitelist
+
+ engine = CreateEngineWithCommitteeSigner(snapshotCache, script);
+
+ NativeContract.Policy.SetWhitelistFeeContract(engine, NativeContract.NEO.Hash, "balanceOf", 1, 0);
+ engine.SnapshotCache.Commit();
+
+ // Whitelisted
+
+ Assert.HasCount(1, engine.Notifications); // Whitelist changed
+ Assert.AreEqual(VMState.HALT, engine.Execute());
+ Assert.AreEqual(0, engine.ResultStack.Pop().GetInteger());
+ Assert.AreEqual(1045290, engine.FeeConsumed);
+
+ // Clean white list
+
+ engine.SnapshotCache.Commit();
+ engine = CreateEngineWithCommitteeSigner(snapshotCache, script);
+
+ Assert.AreEqual(1, NativeContract.Policy.CleanWhitelist(engine, NativeContract.NEO.Hash));
+ Assert.HasCount(1, engine.Notifications); // Whitelist deleted
+ }
+
+ [TestMethod]
+ public void TestSetWhiteListFeeContractNegativeFixedFee()
+ {
+ var snapshotCache = _snapshotCache.CloneCache();
+ var engine = CreateEngineWithCommitteeSigner(snapshotCache);
+
+ // Register a dummy contract
+ UInt160 contractHash;
+ using (var sb = new ScriptBuilder())
+ {
+ sb.Emit(OpCode.RET);
+ var script = sb.ToArray();
+ contractHash = script.ToScriptHash();
+ snapshotCache.DeleteContract(contractHash);
+ var manifest = TestUtils.CreateManifest("dummy", ContractParameterType.Any);
+ manifest.Abi.Methods = [
+ new ContractMethodDescriptor
+ {
+ Name = "foo",
+ Parameters = [],
+ ReturnType = ContractParameterType.Any,
+ Offset = 0,
+ Safe = false
+ }
+ ];
+
+ var contract = TestUtils.GetContract(script, manifest);
+ snapshotCache.AddContract(contractHash, contract);
+ }
+
+ // Invoke SetWhiteListFeeContract with fixedFee negative
+
+ Assert.Throws(() => NativeContract.Policy.SetWhitelistFeeContract(engine, contractHash, "foo", 1, -1L));
+ }
+
+ [TestMethod]
+ public void TestSetWhiteListFeeContractWhenContractNotFound()
+ {
+ var snapshotCache = _snapshotCache.CloneCache();
+ var engine = CreateEngineWithCommitteeSigner(snapshotCache);
+ var randomHash = new UInt160(Crypto.Hash160([1, 2, 3]).ToArray());
+ Assert.ThrowsExactly(() => NativeContract.Policy.SetWhitelistFeeContract(engine, randomHash, "transfer", 3, 10));
+ }
+
+ [TestMethod]
+ public void TestSetWhiteListFeeContractWhenContractNotInAbi()
+ {
+ var snapshotCache = _snapshotCache.CloneCache();
+ var engine = CreateEngineWithCommitteeSigner(snapshotCache);
+ Assert.ThrowsExactly(() => NativeContract.Policy.SetWhitelistFeeContract(engine, NativeContract.NEO.Hash, "noexists", 0, 10));
+ }
+
+ [TestMethod]
+ public void TestSetWhiteListFeeContractWhenArgCountMismatch()
+ {
+ var snapshotCache = _snapshotCache.CloneCache();
+ var engine = CreateEngineWithCommitteeSigner(snapshotCache);
+ // transfer exists with 4 args
+ Assert.ThrowsExactly(() => NativeContract.Policy.SetWhitelistFeeContract(engine, NativeContract.NEO.Hash, "transfer", 0, 10));
+ }
+
+ [TestMethod]
+ public void TestSetWhiteListFeeContractWhenNotCommittee()
+ {
+ var snapshotCache = _snapshotCache.CloneCache();
+ var tx = new Transaction
+ {
+ Version = 0,
+ Nonce = 1,
+ Signers = [new() { Account = UInt160.Zero, Scopes = WitnessScope.Global }],
+ Attributes = [],
+ Witnesses = [new Witness { }],
+ Script = new byte[1],
+ NetworkFee = 0,
+ SystemFee = 0,
+ ValidUntilBlock = 0
+ };
+
+ using var engine = ApplicationEngine.Create(TriggerType.Application, tx, snapshotCache, settings: TestProtocolSettings.Default);
+ Assert.ThrowsExactly(() => NativeContract.Policy.SetWhitelistFeeContract(engine, NativeContract.NEO.Hash, "transfer", 4, 10));
+ }
+
+ [TestMethod]
+ public void TestSetWhiteListFeeContractSetContract()
+ {
+ var snapshotCache = _snapshotCache.CloneCache();
+ var engine = CreateEngineWithCommitteeSigner(snapshotCache);
+ NativeContract.Policy.SetWhitelistFeeContract(engine, NativeContract.NEO.Hash, "transfer", 4, 123_456);
+
+ Assert.IsTrue(NativeContract.Policy.IsWhitelistFeeContract(engine.SnapshotCache, NativeContract.NEO.Hash, "transfer", 4, out var fixedFee));
+ Assert.AreEqual(123_456, fixedFee);
+ }
+
+ private static ApplicationEngine CreateEngineWithCommitteeSigner(DataCache snapshotCache, byte[] script = null)
+ {
+ // Get committe public keys and calculate m
+ var committee = NativeContract.NEO.GetCommittee(snapshotCache);
+ var m = (committee.Length / 2) + 1;
+ var committeeContract = Contract.CreateMultiSigContract(m, committee);
+
+ // Create Tx needed for CheckWitness / CheckCommittee
+ var tx = new Transaction
+ {
+ Version = 0,
+ Nonce = 1,
+ Signers = [new() { Account = committeeContract.ScriptHash, Scopes = WitnessScope.Global }],
+ Attributes = [],
+ Witnesses = [new Witness { InvocationScript = new byte[1], VerificationScript = committeeContract.Script }],
+ Script = script ?? [(byte)OpCode.NOP],
+ NetworkFee = 0,
+ SystemFee = 0,
+ ValidUntilBlock = 0
+ };
+
+ var engine = ApplicationEngine.Create(TriggerType.Application, tx, snapshotCache, settings: TestProtocolSettings.Default);
+ engine.LoadScript(tx.Script);
+
+ return engine;
+ }
}
}