Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
c9662dc
Request blocked funds
shargon Nov 21, 2025
45c442c
fake witness to allow transfers in any NEP17
shargon Nov 21, 2025
a10011f
Use Treasury hash
shargon Nov 22, 2025
7be6d38
6 months
shargon Nov 25, 2025
3f5dc7c
Merge remote-tracking branch 'origin/master-n3' into request-blocked-…
shargon Nov 25, 2025
cbcdfeb
Merge branch 'master-n3' into request-blocked-funds
shargon Nov 27, 2025
bbb20d6
Update src/Neo/SmartContract/Native/PolicyContract.cs
shargon Dec 2, 2025
aacebf6
Merge branch 'master-n3' into request-blocked-funds
shargon Dec 2, 2025
2939a5c
One year time
shargon Dec 2, 2025
6c2ab43
Fix improve
shargon Dec 2, 2025
a59d9cb
Clean code
shargon Dec 2, 2025
9d0974f
Ensure NativeCallingScriptHash change
shargon Dec 2, 2025
0b3e3d8
format
shargon Dec 2, 2025
083f41e
Fix ut
shargon Dec 3, 2025
d85f6c5
Merge branch 'master-n3' into request-blocked-funds
shargon Dec 4, 2025
a73bb20
dotnet format
shargon Dec 5, 2025
50532ee
Fix compilation
ajara87 Dec 6, 2025
f9416b1
Merge pull request #19 from ajara87/pr-4328
shargon Dec 7, 2025
8f70d78
Update src/Neo/SmartContract/Native/PolicyContract.cs
shargon Dec 11, 2025
04febb0
Add events
shargon Dec 11, 2025
364df1f
Anna's and superboy's feedback
shargon Dec 12, 2025
f3341bf
Fix one node network
shargon Dec 14, 2025
ab2e5b3
Superboy and Erik's feedback
shargon Dec 15, 2025
83805d4
Avoid Expect integer type
shargon Dec 15, 2025
6b25341
Invoke manually
shargon Dec 16, 2025
b0f2897
avoid multiple debuggers
shargon Dec 16, 2025
04bb62f
Update tests/Neo.UnitTests/SmartContract/Native/UT_PolicyContract.cs
shargon Dec 17, 2025
1f15256
Update src/Neo/SmartContract/Native/NativeContract.cs
shargon Dec 17, 2025
0e8fd58
Roman's feedback
shargon Dec 17, 2025
b673923
Unify tests
shargon Dec 17, 2025
cefe1f2
Use block for start recover
shargon Dec 17, 2025
f40f1a0
Fix UT
shargon Dec 17, 2025
af77c5d
Merge branch 'master-n3' into request-blocked-funds
shargon Dec 18, 2025
9b51e77
Use current registry for store time
shargon Dec 18, 2025
7a8bec6
Update src/Neo/SmartContract/Native/PolicyContract.cs
shargon Dec 18, 2025
295b29a
Update tests/Neo.UnitTests/SmartContract/Native/UT_PolicyContract.cs
shargon Dec 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/Neo/SmartContract/Native/NativeContract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
96 changes: 92 additions & 4 deletions src/Neo/SmartContract/Native/PolicyContract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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

/// <summary>
/// The event name for the block generation time changed.
/// </summary>
private const string MillisecondsPerBlockChangedEventName = "MillisecondsPerBlockChanged";

private const string RecoveredFundsEventName = "RecoveredFunds";
private const string WhitelistChangedEventName = "WhitelistFeeChanged";

[ContractEvent(Hardfork.HF_Echidna, 0, name: MillisecondsPerBlockChangedEventName,
Expand All @@ -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);
Expand Down Expand Up @@ -583,12 +587,27 @@ internal async ContractTask<bool> 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;
}

Expand All @@ -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;

Expand All @@ -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<UInt160>
{
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<BigInteger>(account, contractHash, "balanceOf", account.ToArray());

if (balance > 0)
{
// transfer
var result = await engine.CallFromNativeContractAsync<bool>(account, contractHash, "transfer",
Copy link
Member Author

@shargon shargon Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@superboyiii I receive an error in your UT in CalculateBonus:

var expectEnd = Ledger.CurrentIndex(snapshot) + 1;
if (expectEnd != end) throw new ArgumentOutOfRangeException(nameof(end));
if (state.BalanceHeight >= end) return BigInteger.Zero;

But it should work with real environment

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Require #4388

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strange... I pull the latest, all passed on my PC.
cb7500fd-0ac8-4697-8188-e0f4523e07f0

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)
{
Expand Down
18 changes: 17 additions & 1 deletion tests/Neo.UnitTests/Extensions/NativeContractExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading