diff --git a/src/Neo/SmartContract/Native/NativeContract.cs b/src/Neo/SmartContract/Native/NativeContract.cs
index 6de22463ae..5578de0189 100644
--- a/src/Neo/SmartContract/Native/NativeContract.cs
+++ b/src/Neo/SmartContract/Native/NativeContract.cs
@@ -364,6 +364,24 @@ protected static void AssertCommittee(ApplicationEngine engine)
throw new InvalidOperationException("Invalid committee signature. It should be a multisig(len(committee) - (len(committee) - 1) / 2)).");
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected static UInt160 AssertAlmostFullCommittee(ApplicationEngine engine)
+ {
+ // Signed by 19/21 committee members
+
+ UInt160 committeeMultiSigAddr;
+ var committees = NativeContract.NEO.GetCommittee(engine.SnapshotCache);
+
+ // Min must be almost the committee address
+ var min = Math.Max(1, committees.Length - (committees.Length - 1) / 2);
+ committeeMultiSigAddr = Contract.CreateMultiSigRedeemScript(Math.Max(min, committees.Length - 2), committees).ToScriptHash();
+
+ if (!engine.CheckWitnessInternal(committeeMultiSigAddr))
+ throw new InvalidOperationException("Invalid committee signature. It should be a multisig(max(1,len(committee) - 2))).");
+
+ return committeeMultiSigAddr;
+ }
+
#region Storage keys
[MethodImpl(MethodImplOptions.AggressiveInlining)]
diff --git a/src/Neo/SmartContract/Native/PolicyContract.cs b/src/Neo/SmartContract/Native/PolicyContract.cs
index e6ff1abb62..478dc0b1f0 100644
--- a/src/Neo/SmartContract/Native/PolicyContract.cs
+++ b/src/Neo/SmartContract/Native/PolicyContract.cs
@@ -16,8 +16,10 @@
using Neo.Persistence;
using Neo.SmartContract.Iterators;
using Neo.SmartContract.Manifest;
+using Neo.VM.Types;
using System;
using System.Buffers.Binary;
+using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
@@ -103,12 +105,13 @@ public sealed class PolicyContract : NativeContract
private readonly StorageKey _millisecondsPerBlock;
private readonly StorageKey _maxValidUntilBlockIncrement;
private readonly StorageKey _maxTraceableBlocks;
+ private const ulong RequiredTimeForRecoverFunds = 365 * 24 * 60 * 60 * 1_000UL; // 1 year in milliseconds
///
/// The event name for the block generation time changed.
///
private const string MillisecondsPerBlockChangedEventName = "MillisecondsPerBlockChanged";
-
+ private const string RecoveredFundsEventName = "RecoveredFunds";
private const string WhitelistChangedEventName = "WhitelistFeeChanged";
[ContractEvent(Hardfork.HF_Echidna, 0, name: MillisecondsPerBlockChangedEventName,
@@ -121,6 +124,7 @@ public sealed class PolicyContract : NativeContract
"argCount", ContractParameterType.Integer,
"fee", ContractParameterType.Any
)]
+ [ContractEvent(Hardfork.HF_Faun, 2, name: RecoveredFundsEventName, "account", ContractParameterType.Hash160)]
internal PolicyContract() : base()
{
_feePerByte = CreateStorageKey(Prefix_FeePerByte);
@@ -583,12 +587,27 @@ internal async ContractTask BlockAccountInternal(ApplicationEngine engine,
var key = CreateStorageKey(Prefix_BlockedAccount, account);
- if (engine.SnapshotCache.Contains(key)) return false;
+ var blockData = engine.SnapshotCache.TryGet(key);
+ if (blockData != null)
+ {
+ // Check if it is stored the recover funds time
+ if (blockData.Value.Length == 0 && engine.IsHardforkEnabled(Hardfork.HF_Faun))
+ {
+ // Don't modify it if already exists
+ blockData.Set(engine.GetTime());
+ }
+
+ return false;
+ }
if (engine.IsHardforkEnabled(Hardfork.HF_Faun))
await NEO.VoteInternal(engine, account, null);
- engine.SnapshotCache.Add(key, new StorageItem([]));
+ engine.SnapshotCache.Add(key,
+ // Set request time for recover funds
+ engine.IsHardforkEnabled(Hardfork.HF_Faun) ? new StorageItem(engine.GetTime())
+ : new StorageItem([]));
+
return true;
}
@@ -597,7 +616,6 @@ private bool UnblockAccount(ApplicationEngine engine, UInt160 account)
{
AssertCommittee(engine);
-
var key = CreateStorageKey(Prefix_BlockedAccount, account);
if (!engine.SnapshotCache.Contains(key)) return false;
@@ -615,6 +633,76 @@ private StorageIterator GetBlockedAccounts(DataCache snapshot)
return new StorageIterator(enumerator, 1, options);
}
+ [ContractMethod(Hardfork.HF_Faun, CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)]
+ internal async ContractTask RecoverFunds(ApplicationEngine engine, UInt160 account, VM.Types.Array extraTokens)
+ {
+ var committeeMultiSigAddr = AssertAlmostFullCommittee(engine);
+
+ // Set request time
+
+ var key = CreateStorageKey(Prefix_BlockedAccount, account);
+ var entry = engine.SnapshotCache.GetAndChange(key, null)
+ ?? throw new InvalidOperationException("Request not found.");
+ if (engine.GetTime() - (BigInteger)entry < RequiredTimeForRecoverFunds)
+ throw new InvalidOperationException("Request must be signed at least 1 year ago.");
+
+ // Validate and collect extra NEP17 tokens
+
+ var validatedTokens = new HashSet
+ {
+ NEO.Hash,
+ GAS.Hash
+ };
+
+ foreach (var tokenItem in extraTokens)
+ {
+ var span = tokenItem.GetSpan();
+ if (span.Length != UInt160.Length)
+ throw new ArgumentException($"Invalid token hash length: expected {UInt160.Length} bytes, got {span.Length} bytes.");
+
+ var contractHash = new UInt160(span);
+
+ // Validate contract exists
+ var contract = ContractManagement.GetContract(engine.SnapshotCache, contractHash);
+ if (contract == null)
+ throw new InvalidOperationException($"Contract {contractHash} does not exist.");
+
+ // Validate contract implements NEP-17 standard
+ if (!contract.Manifest.SupportedStandards.Contains("NEP-17"))
+ throw new InvalidOperationException($"Contract {contractHash} does not implement NEP-17 standard.");
+
+ // Prevent NEO and GAS from being in extraTokens
+ if (contractHash == NEO.Hash || contractHash == GAS.Hash)
+ throw new InvalidOperationException($"NEO and GAS should not be included in extraTokens. They are automatically processed.");
+
+ // Prevent duplicate tokens
+ if (!validatedTokens.Add(contractHash))
+ throw new InvalidOperationException($"Duplicate token {contractHash} in extraTokens.");
+ }
+
+ // Remove and notify
+
+ engine.SendNotification(Hash, RecoveredFundsEventName, [new ByteString(account.ToArray())]);
+
+ // Transfer funds, NEO, GAS and extra NEP17 tokens
+
+ foreach (var contractHash in validatedTokens)
+ {
+ // Check balance
+ var balance = await engine.CallFromNativeContractAsync(account, contractHash, "balanceOf", account.ToArray());
+
+ if (balance > 0)
+ {
+ // transfer
+ var result = await engine.CallFromNativeContractAsync(account, contractHash, "transfer",
+ account.ToArray(), NativeContract.Treasury.Hash.ToArray(), balance, StackItem.Null);
+
+ if (!result)
+ throw new InvalidOperationException($"Transfer of {balance} from {account} to {committeeMultiSigAddr} failed in contract {contractHash}.");
+ }
+ }
+ }
+
[ContractMethod(Hardfork.HF_Faun, CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)]
internal StorageIterator GetWhitelistFeeContracts(DataCache snapshot)
{
diff --git a/tests/Neo.UnitTests/Extensions/NativeContractExtensions.cs b/tests/Neo.UnitTests/Extensions/NativeContractExtensions.cs
index 2f0862c7f1..99baa699b1 100644
--- a/tests/Neo.UnitTests/Extensions/NativeContractExtensions.cs
+++ b/tests/Neo.UnitTests/Extensions/NativeContractExtensions.cs
@@ -80,10 +80,26 @@ public static void UpdateContract(this DataCache snapshot, UInt160 callingScript
public static void DestroyContract(this DataCache snapshot, UInt160 callingScriptHash)
{
+ var persistingBlock = new Block()
+ {
+ Header = new Header()
+ {
+ Index = 0,
+ MerkleRoot = UInt256.Zero,
+ Timestamp = 0,
+ Witness = Witness.Empty,
+ NextConsensus = UInt160.Zero,
+ Nonce = 0,
+ PrevHash = UInt256.Zero,
+ },
+ Transactions = []
+ };
+
var script = new ScriptBuilder();
script.EmitDynamicCall(NativeContract.ContractManagement.Hash, "destroy");
- var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, settings: TestProtocolSettings.Default);
+ var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot,
+ persistingBlock: persistingBlock, settings: TestProtocolSettings.Default);
engine.LoadScript(script.ToArray());
// Fake calling script hash
diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs
index 3d37790438..3336395dd2 100644
--- a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs
+++ b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs
@@ -47,7 +47,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":"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}}"""},
+ {"PolicyContract", """{"id":-7,"updatecounter":0,"hash":"0xcc5e4edd9f5f8dba8bb65734541df7a1c081c67b","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":2681632925},"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":"recoverFunds","parameters":[{"name":"account","type":"Hash160"},{"name":"extraTokens","type":"Array"}],"returntype":"Void","offset":84,"safe":false},{"name":"removeWhitelistFeeContract","parameters":[{"name":"contractHash","type":"Hash160"},{"name":"method","type":"String"},{"name":"argCount","type":"Integer"}],"returntype":"Void","offset":91,"safe":false},{"name":"setAttributeFee","parameters":[{"name":"attributeType","type":"Integer"},{"name":"value","type":"Integer"}],"returntype":"Void","offset":98,"safe":false},{"name":"setExecFeeFactor","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":105,"safe":false},{"name":"setFeePerByte","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":112,"safe":false},{"name":"setMaxTraceableBlocks","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":119,"safe":false},{"name":"setMaxValidUntilBlockIncrement","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":126,"safe":false},{"name":"setMillisecondsPerBlock","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":133,"safe":false},{"name":"setStoragePrice","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":140,"safe":false},{"name":"setWhitelistFeeContract","parameters":[{"name":"contractHash","type":"Hash160"},{"name":"method","type":"String"},{"name":"argCount","type":"Integer"},{"name":"fixedFee","type":"Integer"}],"returntype":"Void","offset":147,"safe":false},{"name":"unblockAccount","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Boolean","offset":154,"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"}]},{"name":"RecoveredFunds","parameters":[{"name":"account","type":"Hash160"}]}]},"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 19f0788a77..e7cc086207 100644
--- a/tests/Neo.UnitTests/SmartContract/Native/UT_PolicyContract.cs
+++ b/tests/Neo.UnitTests/SmartContract/Native/UT_PolicyContract.cs
@@ -24,7 +24,6 @@
using System;
using System.Linq;
using System.Numerics;
-using System.Reflection;
using Boolean = Neo.VM.Types.Boolean;
namespace Neo.UnitTests.SmartContract.Native
@@ -216,6 +215,105 @@ public void Check_SetBaseExecFee()
Assert.AreEqual(50, ret.GetInteger());
}
+ [TestMethod]
+ public void Check_RecoverFunds_CompleteFlow()
+ {
+ var snapshot = _snapshotCache.CloneCache();
+
+ // Get almost full committee address
+ var committeeMultiSigAddr = NativeContract.NEO.GetCommitteeAddress(snapshot);
+ var committees = NativeContract.NEO.GetCommittee(snapshot);
+ var min = Math.Max(1, committees.Length - (committees.Length - 1) / 2);
+ var committeeFullMultiSigAddr = Contract.CreateMultiSigRedeemScript(Math.Max(min, committees.Length - 2), committees).ToScriptHash();
+ // Create a blocked account
+ UInt160 blockedAccount = UInt160.Parse("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff01");
+ ulong startTime = 1000000;
+ ulong requiredTime = 365UL * 24 * 60 * 60 * 1_000; // Actual value from code
+ ulong finishTime = startTime + requiredTime + 1000; // More than required time
+
+ // Block 1: For recoverFundsStart
+ Block blockStart = new()
+ {
+ Header = new Header
+ {
+ PrevHash = UInt256.Zero,
+ MerkleRoot = UInt256.Zero,
+ Index = 1000,
+ Timestamp = startTime,
+ NextConsensus = UInt160.Zero,
+ Witness = null!
+ },
+ Transactions = []
+ };
+
+ // Block 2: For recoverFundsFinish (more than 1 year later)
+ Block blockFinish = new()
+ {
+ Header = new Header
+ {
+ PrevHash = UInt256.Zero,
+ MerkleRoot = UInt256.Zero,
+ Index = 2000,
+ Timestamp = finishTime,
+ NextConsensus = UInt160.Zero,
+ Witness = null!
+ },
+ Transactions = []
+ };
+
+ // Try Without signature
+ Assert.ThrowsExactly(() =>
+ {
+ NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(), blockStart,
+ "recoverFundsFinish",
+ new ContractParameter(ContractParameterType.Hash160) { Value = UInt160.Zero },
+ new ContractParameter(ContractParameterType.Array) { Value = System.Array.Empty() });
+ });
+ // Step 1: Block the account
+ var ret = NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(committeeMultiSigAddr), blockStart,
+ "blockAccount",
+ new ContractParameter(ContractParameterType.Hash160) { Value = blockedAccount });
+ Assert.IsInstanceOfType(ret, typeof(Boolean));
+ Assert.IsTrue(ret.GetBoolean());
+ Assert.IsTrue(NativeContract.Policy.IsBlocked(snapshot, blockedAccount));
+
+ // Step 2: Set account balances (NEO and GAS)
+ BigInteger neoBalance = 100000 * NativeContract.NEO.Factor; // 100000 NEO
+ BigInteger gasBalance = 50000 * NativeContract.GAS.Factor; // 50000 GAS
+
+ // Set NEO balance
+ var neoKey = NativeContract.NEO.CreateStorageKey(20, blockedAccount);
+ var neoEntry = snapshot.GetAndChange(neoKey, () => new StorageItem(new NeoToken.NeoAccountState()));
+ neoEntry.GetInteroperable().Balance = neoBalance;
+
+ // Set GAS balance
+ var gasKey = NativeContract.GAS.CreateStorageKey(20, blockedAccount);
+ var gasEntry = snapshot.GetAndChange(gasKey, () => new StorageItem(new AccountState()));
+ gasEntry.GetInteroperable().Balance = gasBalance;
+
+ // Verify balances are set
+ Assert.AreEqual(neoBalance, NativeContract.NEO.BalanceOf(snapshot, blockedAccount));
+ Assert.AreEqual(gasBalance, NativeContract.GAS.BalanceOf(snapshot, blockedAccount));
+
+ // Step 3: Call recoverFundsFinish (after required time has passed)
+ // This should transfer all funds to Treasury
+ NativeContract.Policy.Call(snapshot, new Nep17NativeContractExtensions.ManualWitness(committeeFullMultiSigAddr), blockFinish,
+ "recoverFunds",
+ new ContractParameter(ContractParameterType.Hash160) { Value = blockedAccount },
+ new ContractParameter(ContractParameterType.Array) { Value = System.Array.Empty() });
+
+ // Step 5: Verify balances were transferred to Treasury
+ Assert.AreEqual(BigInteger.Zero, NativeContract.NEO.BalanceOf(snapshot, blockedAccount));
+ Assert.AreEqual(BigInteger.Zero, NativeContract.GAS.BalanceOf(snapshot, blockedAccount));
+
+ // Verify Treasury received the funds
+ var treasuryNeoBalance = NativeContract.NEO.BalanceOf(snapshot, NativeContract.Treasury.Hash);
+ var treasuryGasBalance = NativeContract.GAS.BalanceOf(snapshot, NativeContract.Treasury.Hash);
+ // Treasury should have received the funds (exact balance depends on initial Treasury balance)
+ Assert.IsTrue(treasuryNeoBalance >= neoBalance, "Treasury should have received NEO");
+ Assert.IsTrue(treasuryGasBalance >= gasBalance, "Treasury should have received GAS");
+ }
+
[TestMethod]
public void Check_SetStoragePrice()
{