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() {