diff --git a/docs/native-contracts-api.md b/docs/native-contracts-api.md
new file mode 100644
index 0000000000..470d92a730
--- /dev/null
+++ b/docs/native-contracts-api.md
@@ -0,0 +1,209 @@
+# Native Contracts API
+Native contracts are the contracts that are implemented in the Neo blockchain,
+and native contract APIsare the methods that are provided by the native contracts.
+
+When calling a native contract method by transaction script, there are several tips and notes:
+1. A part of native contract methods require CallFlags. If no such CallFlags is provided, the call will be failed.
+2. Some native contract methods are only allowed to be called before or after a certain hardfork.
+3. A native contract method may have different behaviors in different hardforks.
+
+## Table of Contents
+
+1. [ContractManagement](#contractmanagement)
+2. [StdLib](#stdlib)
+3. [CryptoLib](#cryptolib)
+4. [LedgerContract](#ledgercontract)
+5. [NeoToken](#neotoken)
+6. [GasToken](#gastoken)
+7. [PolicyContract](#policycontract)
+8. [RoleManagement](#rolemanagement)
+9. [OracleContract](#oraclecontract)
+10. [Notary](#notary)
+
+## ContractManagement
+
+| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork |
+|--------|---------|------------|--------------|---------|-------------|------------|----------|
+| getMinimumDeploymentFee | Gets the minimum deployment fee for deploying a contract. | -- | Int64 | 1<<15 | 0 | ReadStates | -- |
+| setMinimumDeploymentFee | Sets the minimum deployment fee for deploying a contract. Only committee members can call this method. | BigInteger(*value*) | Void | 1<<15 | 0 | States | -- |
+| getContract | Gets the deployed contract with the specified hash. | UInt160(*hash*) | ContractState | 1<<15 | 0 | ReadStates | -- |
+| isContract | Check if exists the deployed contract with the specified hash. | UInt160(*hash*) | Boolean | 1<<14 | 0 | ReadStates | HF_Echidna |
+| getContractById | Maps specified ID to deployed contract. | Int32(*id*) | ContractState | 1<<15 | 0 | ReadStates | -- |
+| getContractHashes | Gets hashes of all non native deployed contracts. | -- | IIterator | 1<<15 | 0 | ReadStates | -- |
+| hasMethod | Check if a method exists in a contract. | UInt160(*hash*), String(*method*), Int32(*pcount*) | Boolean | 1<<15 | 0 | ReadStates | -- |
+| deploy | Deploys a contract. It needs to pay the deployment fee and storage fee. | Byte[](*nefFile*), Byte[](*manifest*) | ContractState | 0 | 0 | States,AllowNotify | -- |
+| deploy | Deploys a contract. It needs to pay the deployment fee and storage fee. | Byte[](*nefFile*), Byte[](*manifest*), StackItem(*data*) | ContractState | 0 | 0 | States,AllowNotify | -- |
+| update | Updates a contract. It needs to pay the storage fee. | Byte[](*nefFile*), Byte[](*manifest*) | Void | 0 | 0 | States,AllowNotify | -- |
+| update | Updates a contract. It needs to pay the storage fee. | Byte[](*nefFile*), Byte[](*manifest*), StackItem(*data*) | Void | 0 | 0 | States,AllowNotify | -- |
+| destroy | Destroys a contract. | -- | Void | 1<<15 | 0 | States,AllowNotify | -- |
+
+
+## StdLib
+
+| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork |
+|--------|---------|------------|--------------|---------|-------------|------------|----------|
+| serialize | -- | StackItem(*item*) | Byte[] | 1<<12 | 0 | -- | -- |
+| deserialize | -- | Byte[](*data*) | StackItem | 1<<14 | 0 | -- | -- |
+| jsonSerialize | -- | StackItem(*item*) | Byte[] | 1<<12 | 0 | -- | -- |
+| jsonDeserialize | -- | Byte[](*json*) | StackItem | 1<<14 | 0 | -- | -- |
+| itoa | Converts an integer to a String. | BigInteger(*value*) | String | 1<<12 | 0 | -- | -- |
+| itoa | Converts an integer to a String. | BigInteger(*value*), Int32(*base*) | String | 1<<12 | 0 | -- | -- |
+| atoi | Converts a String to an integer. | String(*value*) | BigInteger | 1<<6 | 0 | -- | -- |
+| atoi | Converts a String to an integer. | String(*value*), Int32(*base*) | BigInteger | 1<<6 | 0 | -- | -- |
+| base64Encode | Encodes a byte array into a base64 String. | Byte[](*data*) | String | 1<<5 | 0 | -- | -- |
+| base64Decode | Decodes a byte array from a base64 String. | String(*s*) | Byte[] | 1<<5 | 0 | -- | -- |
+| base64UrlEncode | Encodes a byte array into a base64Url string. | String(*data*) | String | 1<<5 | 0 | -- | HF_Echidna |
+| base64UrlDecode | Decodes a byte array from a base64Url string. | String(*s*) | String | 1<<5 | 0 | -- | HF_Echidna |
+| base58Encode | Encodes a byte array into a base58 String. | Byte[](*data*) | String | 1<<13 | 0 | -- | -- |
+| base58Decode | Decodes a byte array from a base58 String. | String(*s*) | Byte[] | 1<<10 | 0 | -- | -- |
+| base58CheckEncode | Converts a byte array to its equivalent String representation that is encoded with base-58 digits. The encoded String contains the checksum of the binary data. | Byte[](*data*) | String | 1<<16 | 0 | -- | -- |
+| base58CheckDecode | Converts the specified String, which encodes binary data as base-58 digits, to an equivalent byte array. The encoded String contains the checksum of the binary data. | String(*s*) | Byte[] | 1<<16 | 0 | -- | -- |
+| hexEncode | -- | Byte[](*bytes*) | String | 1<<5 | 0 | -- | HF_Faun |
+| hexDecode | -- | String(*str*) | Byte[] | 1<<5 | 0 | -- | HF_Faun |
+| memoryCompare | -- | Byte[](*str1*), Byte[](*str2*) | Int32 | 1<<5 | 0 | -- | -- |
+| memorySearch | -- | Byte[](*mem*), Byte[](*value*) | Int32 | 1<<6 | 0 | -- | -- |
+| memorySearch | -- | Byte[](*mem*), Byte[](*value*), Int32(*start*) | Int32 | 1<<6 | 0 | -- | -- |
+| memorySearch | -- | Byte[](*mem*), Byte[](*value*), Int32(*start*), Boolean(*backward*) | Int32 | 1<<6 | 0 | -- | -- |
+| stringSplit | -- | String(*str*), String(*separator*) | String[] | 1<<8 | 0 | -- | -- |
+| stringSplit | -- | String(*str*), String(*separator*), Boolean(*removeEmptyEntries*) | String[] | 1<<8 | 0 | -- | -- |
+| strLen | -- | String(*str*) | Int32 | 1<<8 | 0 | -- | -- |
+
+
+## CryptoLib
+
+| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork |
+|--------|---------|------------|--------------|---------|-------------|------------|----------|
+| bls12381Serialize | Serialize a bls12381 point. | InteropInterface(*g*) | Byte[] | 1<<19 | 0 | -- | -- |
+| bls12381Deserialize | Deserialize a bls12381 point. | Byte[](*data*) | InteropInterface | 1<<19 | 0 | -- | -- |
+| bls12381Equal | Determines whether the specified points are equal. | InteropInterface(*x*), InteropInterface(*y*) | Boolean | 1<<5 | 0 | -- | -- |
+| bls12381Add | Add operation of two points. | InteropInterface(*x*), InteropInterface(*y*) | InteropInterface | 1<<19 | 0 | -- | -- |
+| bls12381Mul | Mul operation of gt point and multiplier | InteropInterface(*x*), Byte[](*mul*), Boolean(*neg*) | InteropInterface | 1<<21 | 0 | -- | -- |
+| bls12381Pairing | Pairing operation of g1 and g2 | InteropInterface(*g1*), InteropInterface(*g2*) | InteropInterface | 1<<23 | 0 | -- | -- |
+| recoverSecp256K1 | Recovers the public key from a secp256k1 signature in a single byte array format. | Byte[](*messageHash*), Byte[](*signature*) | Byte[] | 1<<15 | 0 | -- | HF_Echidna |
+| ripemd160 | Computes the hash value for the specified byte array using the ripemd160 algorithm. | Byte[](*data*) | Byte[] | 1<<15 | 0 | -- | -- |
+| sha256 | Computes the hash value for the specified byte array using the sha256 algorithm. | Byte[](*data*) | Byte[] | 1<<15 | 0 | -- | -- |
+| murmur32 | Computes the hash value for the specified byte array using the murmur32 algorithm. | Byte[](*data*), UInt32(*seed*) | Byte[] | 1<<13 | 0 | -- | -- |
+| keccak256 | Computes the hash value for the specified byte array using the keccak256 algorithm. | Byte[](*data*) | Byte[] | 1<<15 | 0 | -- | HF_Cockatrice |
+| verifyWithECDsa | Verifies that a digital signature is appropriate for the provided key and message using the ECDSA algorithm. | Byte[](*message*), Byte[](*pubkey*), Byte[](*signature*), NamedCurveHash(*curveHash*) | Boolean | 1<<15 | 0 | -- | HF_Cockatrice |
+| verifyWithECDsa | -- | Byte[](*message*), Byte[](*pubkey*), Byte[](*signature*), NamedCurveHash(*curve*) | Boolean | 1<<15 | 0 | -- | Deprecated in HF_Cockatrice |
+| verifyWithEd25519 | Verifies that a digital signature is appropriate for the provided key and message using the Ed25519 algorithm. | Byte[](*message*), Byte[](*pubkey*), Byte[](*signature*) | Boolean | 1<<15 | 0 | -- | HF_Echidna |
+
+
+## LedgerContract
+
+| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork |
+|--------|---------|------------|--------------|---------|-------------|------------|----------|
+| currentHash | Gets the hash of the current block. | -- | UInt256 | 1<<15 | 0 | ReadStates | -- |
+| currentIndex | Gets the index of the current block. | -- | UInt32 | 1<<15 | 0 | ReadStates | -- |
+| getBlock | -- | Byte[](*indexOrHash*) | TrimmedBlock | 1<<15 | 0 | ReadStates | -- |
+| getTransaction | -- | UInt256(*hash*) | Transaction | 1<<15 | 0 | ReadStates | -- |
+| getTransactionSigners | -- | UInt256(*hash*) | Signer[] | 1<<15 | 0 | ReadStates | -- |
+| getTransactionVMState | -- | UInt256(*hash*) | VMState | 1<<15 | 0 | ReadStates | -- |
+| getTransactionHeight | -- | UInt256(*hash*) | Int32 | 1<<15 | 0 | ReadStates | -- |
+| getTransactionFromBlock | -- | Byte[](*blockIndexOrHash*), Int32(*txIndex*) | Transaction | 1<<16 | 0 | ReadStates | -- |
+
+
+## NeoToken
+
+| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork |
+|--------|---------|------------|--------------|---------|-------------|------------|----------|
+| totalSupply | -- | -- | BigInteger | 1<<15 | 0 | ReadStates | -- |
+| setGasPerBlock | Sets the amount of GAS generated in each block. Only committee members can call this method. | BigInteger(*gasPerBlock*) | Void | 1<<15 | 0 | States | -- |
+| getGasPerBlock | Gets the amount of GAS generated in each block. | -- | BigInteger | 1<<15 | 0 | ReadStates | -- |
+| setRegisterPrice | Sets the fees to be paid to register as a candidate. Only committee members can call this method. | Int64(*registerPrice*) | Void | 1<<15 | 0 | States | -- |
+| getRegisterPrice | Gets the fees to be paid to register as a candidate. | -- | Int64 | 1<<15 | 0 | ReadStates | -- |
+| unclaimedGas | Get the amount of unclaimed GAS in the specified account. | UInt160(*account*), UInt32(*end*) | BigInteger | 1<<17 | 0 | ReadStates | -- |
+| onNEP17Payment | Handles the payment of GAS. | UInt160(*from*), BigInteger(*amount*), StackItem(*data*) | Void | 0 | 0 | States,AllowNotify | HF_Echidna |
+| registerCandidate | Registers a candidate. | ECPoint(*pubkey*) | Boolean | 0 | 0 | States | Deprecated in HF_Echidna |
+| registerCandidate | Registers a candidate. | ECPoint(*pubkey*) | Boolean | 0 | 0 | States,AllowNotify | HF_Echidna |
+| unregisterCandidate | Unregisters a candidate. | ECPoint(*pubkey*) | Boolean | 1<<16 | 0 | States | Deprecated in HF_Echidna |
+| unregisterCandidate | Unregisters a candidate. | ECPoint(*pubkey*) | Boolean | 1<<16 | 0 | States,AllowNotify | HF_Echidna |
+| vote | Votes for a candidate. | UInt160(*account*), ECPoint(*voteTo*) | Boolean | 1<<16 | 0 | States | Deprecated in HF_Echidna |
+| vote | Votes for a candidate. | UInt160(*account*), ECPoint(*voteTo*) | Boolean | 1<<16 | 0 | States,AllowNotify | HF_Echidna |
+| getCandidates | Gets the first 256 registered candidates. | -- | ValueTuple`2[] | 1<<22 | 0 | ReadStates | -- |
+| getAllCandidates | Gets the registered candidates iterator. | -- | IIterator | 1<<22 | 0 | ReadStates | -- |
+| getCandidateVote | Gets votes from specific candidate. | ECPoint(*pubKey*) | BigInteger | 1<<15 | 0 | ReadStates | -- |
+| getCommittee | Gets all the members of the committee. | -- | ECPoint[] | 1<<16 | 0 | ReadStates | -- |
+| getAccountState | Get account state. | UInt160(*account*) | NeoAccountState | 1<<15 | 0 | ReadStates | -- |
+| getCommitteeAddress | Gets the address of the committee. | -- | UInt160 | 1<<16 | 0 | ReadStates | HF_Cockatrice |
+| getNextBlockValidators | Gets the validators of the next block. | -- | ECPoint[] | 1<<16 | 0 | ReadStates | -- |
+| balanceOf | Gets the balance of the specified account. | UInt160(*account*) | BigInteger | 1<<15 | 0 | ReadStates | -- |
+| transfer | -- | UInt160(*from*), UInt160(*to*), BigInteger(*amount*), StackItem(*data*) | Boolean | 1<<17 | 50 | All | -- |
+| symbol | -- | -- | String | 0 | 0 | -- | -- |
+| decimals | -- | -- | Byte | 0 | 0 | -- | -- |
+
+
+## GasToken
+
+| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork |
+|--------|---------|------------|--------------|---------|-------------|------------|----------|
+| totalSupply | Gets the total supply of the token. | -- | BigInteger | 1<<15 | 0 | ReadStates | -- |
+| balanceOf | Gets the balance of the specified account. | UInt160(*account*) | BigInteger | 1<<15 | 0 | ReadStates | -- |
+| transfer | -- | UInt160(*from*), UInt160(*to*), BigInteger(*amount*), StackItem(*data*) | Boolean | 1<<17 | 50 | All | -- |
+| symbol | -- | -- | String | 0 | 0 | -- | -- |
+| decimals | -- | -- | Byte | 0 | 0 | -- | -- |
+
+
+## PolicyContract
+
+| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork |
+|--------|---------|------------|--------------|---------|-------------|------------|----------|
+| getFeePerByte | Gets the network fee per transaction byte. | -- | Int64 | 1<<15 | 0 | ReadStates | -- |
+| getExecFeeFactor | Gets the execution fee factor. This is a multiplier that can be adjusted by the committee to adjust the system fees for transactions. | -- | UInt32 | 1<<15 | 0 | ReadStates | -- |
+| getExecPicoFeeFactor | Gets the execution fee factor. This is a multiplier that can be adjusted by the committee to adjust the system fees for transactions. | -- | BigInteger | 1<<15 | 0 | ReadStates | HF_Faun |
+| getStoragePrice | Gets the storage price. | -- | UInt32 | 1<<15 | 0 | ReadStates | -- |
+| getMillisecondsPerBlock | Gets the block generation time in milliseconds. | -- | UInt32 | 1<<15 | 0 | ReadStates | HF_Echidna |
+| getMaxValidUntilBlockIncrement | Gets the upper increment size of blockchain height (in blocks) exceeding that a transaction should fail validation. | -- | UInt32 | 1<<15 | 0 | ReadStates | HF_Echidna |
+| getMaxTraceableBlocks | Gets the length of the chain accessible to smart contracts. | -- | UInt32 | 1<<15 | 0 | ReadStates | HF_Echidna |
+| getAttributeFee | Gets the fee for attribute before Echidna hardfork. NotaryAssisted attribute type not supported. | Byte(*attributeType*) | UInt32 | 1<<15 | 0 | ReadStates | Deprecated in HF_Echidna |
+| getAttributeFee | Gets the fee for attribute after Echidna hardfork. NotaryAssisted attribute type supported. | Byte(*attributeType*) | UInt32 | 1<<15 | 0 | ReadStates | HF_Echidna |
+| isBlocked | Determines whether the specified account is blocked. | UInt160(*account*) | Boolean | 1<<15 | 0 | ReadStates | -- |
+| removeWhitelistFeeContract | Remove whitelisted Fee contracts | UInt160(*contractHash*), String(*method*), Int32(*argCount*) | Void | 1<<15 | 0 | States,AllowNotify | HF_Faun |
+| setWhitelistFeeContract | Set whitelisted Fee contracts | UInt160(*contractHash*), String(*method*), Int32(*argCount*), Int64(*fixedFee*) | Void | 1<<15 | 0 | States,AllowNotify | HF_Faun |
+| setMillisecondsPerBlock | Sets the block generation time in milliseconds. | UInt32(*value*) | Void | 1<<15 | 0 | States,AllowNotify | HF_Echidna |
+| setAttributeFee | Sets the fee for attribute before Echidna hardfork. NotaryAssisted attribute type not supported. | Byte(*attributeType*), UInt32(*value*) | Void | 1<<15 | 0 | States | Deprecated in HF_Echidna |
+| setAttributeFee | Sets the fee for attribute after Echidna hardfork. NotaryAssisted attribute type supported. | Byte(*attributeType*), UInt32(*value*) | Void | 1<<15 | 0 | States | HF_Echidna |
+| setFeePerByte | -- | Int64(*value*) | Void | 1<<15 | 0 | States | -- |
+| setExecFeeFactor | -- | UInt64(*value*) | Void | 1<<15 | 0 | States | -- |
+| setStoragePrice | -- | UInt32(*value*) | Void | 1<<15 | 0 | States | -- |
+| setMaxValidUntilBlockIncrement | -- | UInt32(*value*) | Void | 1<<15 | 0 | States | HF_Echidna |
+| setMaxTraceableBlocks | Sets the length of the chain accessible to smart contracts. | UInt32(*value*) | Void | 1<<15 | 0 | States | HF_Echidna |
+| blockAccount | -- | UInt160(*account*) | Boolean | 1<<15 | 0 | States | -- |
+| unblockAccount | -- | UInt160(*account*) | Boolean | 1<<15 | 0 | States | -- |
+| getBlockedAccounts | -- | -- | StorageIterator | 1<<15 | 0 | ReadStates | HF_Faun |
+| getWhitelistFeeContracts | -- | -- | StorageIterator | 1<<15 | 0 | ReadStates | HF_Faun |
+
+
+## RoleManagement
+
+| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork |
+|--------|---------|------------|--------------|---------|-------------|------------|----------|
+| getDesignatedByRole | Gets the list of nodes for the specified role. | Role(*role*), UInt32(*index*) | ECPoint[] | 1<<15 | 0 | ReadStates | -- |
+| designateAsRole | -- | Role(*role*), ECPoint[](*nodes*) | Void | 1<<15 | 0 | States,AllowNotify | -- |
+
+
+## OracleContract
+
+| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork |
+|--------|---------|------------|--------------|---------|-------------|------------|----------|
+| setPrice | Sets the price for an Oracle request. Only committee members can call this method. | Int64(*price*) | Void | 1<<15 | 0 | States | -- |
+| getPrice | Gets the price for an Oracle request. | -- | Int64 | 1<<15 | 0 | ReadStates | -- |
+| finish | Finishes an Oracle response. | -- | Void | 0 | 0 | All | -- |
+| request | -- | String(*url*), String(*filter*), String(*callback*), StackItem(*userData*), Int64(*gasForResponse*) | Void | 0 | 0 | States,AllowNotify | -- |
+| verify | -- | -- | Boolean | 1<<15 | 0 | -- | -- |
+
+
+## Notary
+
+| Method | Summary | Parameters | Return Value | CPU fee | Storage fee | Call Flags | Hardfork |
+|--------|---------|------------|--------------|---------|-------------|------------|----------|
+| verify | Verify checks whether the transaction is signed by one of the notaries and ensures whether deposited amount of GAS is enough to pay the actual sender's fee. | Byte[](*signature*) | Boolean | 1<<15 | 0 | ReadStates | -- |
+| onNEP17Payment | OnNEP17Payment is a callback that accepts GAS transfer as Notary deposit. It also sets the deposit's lock height after which deposit can be withdrawn. | UInt160(*from*), BigInteger(*amount*), StackItem(*data*) | Void | 1<<15 | 0 | States | -- |
+| lockDepositUntil | Lock asset until the specified height is unlocked. | UInt160(*account*), UInt32(*till*) | Boolean | 1<<15 | 0 | States | -- |
+| expirationOf | ExpirationOf returns deposit lock height for specified address. | UInt160(*account*) | UInt32 | 1<<15 | 0 | ReadStates | -- |
+| balanceOf | BalanceOf returns deposited GAS amount for specified address. | UInt160(*account*) | BigInteger | 1<<15 | 0 | ReadStates | -- |
+| withdraw | Withdraw sends all deposited GAS for "from" address to "to" address. If "to" address is not specified, then "from" will be used as a sender. | UInt160(*from*), UInt160(*to*) | Boolean | 1<<15 | 0 | All | -- |
+| getMaxNotValidBeforeDelta | GetMaxNotValidBeforeDelta is Notary contract method and returns the maximum NotValidBefore delta. | -- | UInt32 | 1<<15 | 0 | ReadStates | -- |
+| setMaxNotValidBeforeDelta | SetMaxNotValidBeforeDelta is Notary contract method and sets the maximum NotValidBefore delta. | UInt32(*value*) | Void | 1<<15 | 0 | States | -- |
+
+
diff --git a/src/Neo/SmartContract/ApplicationEngine.cs b/src/Neo/SmartContract/ApplicationEngine.cs
index 2d66654fee..12df4dc6e8 100644
--- a/src/Neo/SmartContract/ApplicationEngine.cs
+++ b/src/Neo/SmartContract/ApplicationEngine.cs
@@ -328,6 +328,14 @@ protected static void OnSysCall(ExecutionEngine engine, Instruction instruction)
/// The amount of GAS, in the unit of picoGAS, 1 picoGAS = 1e-12 GAS, to be added.
protected internal void AddFee(BigInteger picoGas)
{
+ // Check whitelist
+
+ if (CurrentContext?.GetState()?.WhiteListed == true)
+ {
+ // The execution is whitelisted
+ return;
+ }
+
_feeConsumed = _feeConsumed + picoGas;
if (_feeConsumed > _feeAmount)
throw new InvalidOperationException("Insufficient GAS.");
@@ -368,12 +376,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 * ApplicationEngine.FeeFactor);
+ 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 f4c2ccd03f..a8b9430a0b 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 7453ceb947..cf78d2c083 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,10 +435,15 @@ 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 picoGAS, 1 picoGAS = 1e-12 GAS
- engine.AddFee(
- (method.CpuFee * engine.ExecFeePicoFactor) +
- (method.StorageFee * engine.StoragePrice * ApplicationEngine.FeeFactor));
+ // 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 picoGAS, 1 picoGAS = 1e-12 GAS
+ engine.AddFee(
+ (method.CpuFee * engine.ExecFeePicoFactor) +
+ (method.StorageFee * engine.StoragePrice * ApplicationEngine.FeeFactor));
+ }
List
private const string MillisecondsPerBlockChangedEventName = "MillisecondsPerBlockChanged";
+ private const string WhitelistChangedEventName = "WhitelistFeeChanged";
+
[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);
@@ -290,6 +302,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.
///
@@ -473,5 +592,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 a811505502..c6d36a04e6 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":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQA==","checksum":65467259},"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":"getExecPicoFeeFactor","parameters":[],"returntype":"Integer","offset":28,"safe":true},{"name":"getFeePerByte","parameters":[],"returntype":"Integer","offset":35,"safe":true},{"name":"getMaxTraceableBlocks","parameters":[],"returntype":"Integer","offset":42,"safe":true},{"name":"getMaxValidUntilBlockIncrement","parameters":[],"returntype":"Integer","offset":49,"safe":true},{"name":"getMillisecondsPerBlock","parameters":[],"returntype":"Integer","offset":56,"safe":true},{"name":"getStoragePrice","parameters":[],"returntype":"Integer","offset":63,"safe":true},{"name":"isBlocked","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":70,"safe":true},{"name":"setAttributeFee","parameters":[{"name":"attributeType","type":"Integer"},{"name":"value","type":"Integer"}],"returntype":"Void","offset":77,"safe":false},{"name":"setExecFeeFactor","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":84,"safe":false},{"name":"setFeePerByte","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":91,"safe":false},{"name":"setMaxTraceableBlocks","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":98,"safe":false},{"name":"setMaxValidUntilBlockIncrement","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":105,"safe":false},{"name":"setMillisecondsPerBlock","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":112,"safe":false},{"name":"setStoragePrice","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":119,"safe":false},{"name":"unblockAccount","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":126,"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":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQA==","checksum":751055395},"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":"getExecPicoFeeFactor","parameters":[],"returntype":"Integer","offset":28,"safe":true},{"name":"getFeePerByte","parameters":[],"returntype":"Integer","offset":35,"safe":true},{"name":"getMaxTraceableBlocks","parameters":[],"returntype":"Integer","offset":42,"safe":true},{"name":"getMaxValidUntilBlockIncrement","parameters":[],"returntype":"Integer","offset":49,"safe":true},{"name":"getMillisecondsPerBlock","parameters":[],"returntype":"Integer","offset":56,"safe":true},{"name":"getStoragePrice","parameters":[],"returntype":"Integer","offset":63,"safe":true},{"name":"getWhitelistFeeContracts","parameters":[],"returntype":"InteropInterface","offset":70,"safe":true},{"name":"isBlocked","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":77,"safe":true},{"name":"removeWhitelistFeeContract","parameters":[{"name":"contractHash","type":"Hash160"},{"name":"method","type":"String"},{"name":"argCount","type":"Integer"}],"returntype":"Void","offset":84,"safe":false},{"name":"setAttributeFee","parameters":[{"name":"attributeType","type":"Integer"},{"name":"value","type":"Integer"}],"returntype":"Void","offset":91,"safe":false},{"name":"setExecFeeFactor","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":98,"safe":false},{"name":"setFeePerByte","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":105,"safe":false},{"name":"setMaxTraceableBlocks","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":112,"safe":false},{"name":"setMaxValidUntilBlockIncrement","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":119,"safe":false},{"name":"setMillisecondsPerBlock","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":126,"safe":false},{"name":"setStoragePrice","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":133,"safe":false},{"name":"setWhitelistFeeContract","parameters":[{"name":"contractHash","type":"Hash160"},{"name":"method","type":"String"},{"name":"argCount","type":"Integer"},{"name":"fixedFee","type":"Integer"}],"returntype":"Void","offset":140,"safe":false},{"name":"unblockAccount","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":147,"safe":false}],"events":[{"name":"MillisecondsPerBlockChanged","parameters":[{"name":"old","type":"Integer"},{"name":"new","type":"Integer"}]},{"name":"WhitelistFeeChanged","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 ba0471e1a1..d9f234fd03 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;
+ }
}
}