Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
83 changes: 83 additions & 0 deletions src/Neo/SmartContract/Native/Governance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Neo.Network.P2P.Payloads;
using Neo.Persistence;
using Neo.SmartContract.Iterators;
using Neo.SmartContract.Manifest;
using Neo.VM;
using Neo.VM.Types;
using System.Buffers.Binary;
Expand Down Expand Up @@ -58,6 +59,11 @@ public sealed class Governance : NativeContract

internal Governance() : base(-13) { }

protected override void OnManifestCompose(IsHardforkEnabledDelegate hfChecker, uint blockHeight, ContractManifest manifest)
{
manifest.SupportedStandards = ["NEP-27"];
}

internal override async ContractTask InitializeAsync(ApplicationEngine engine, Hardfork? hardFork)
{
if (hardFork == ActiveIn)
Expand Down Expand Up @@ -333,6 +339,29 @@ internal async ContractTask<bool> VoteInternal(ApplicationEngine engine, UInt160
return snapshot.TryGet(key)?.GetInteroperable<NeoAccountState>().VoteTo;
}

/// <summary>
/// Gets the account state including balance, balance height, and vote target.
/// </summary>
/// <param name="snapshot">The snapshot used to read data.</param>
/// <param name="account">The account address.</param>
/// <returns>A struct containing balance, balance height, and vote target.</returns>
[ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)]
public Struct GetAccountState(IReadOnlyStore snapshot, UInt160 account)
{
BigInteger balance = TokenManagement.BalanceOf(snapshot, NeoTokenId, account);
StorageKey key = CreateStorageKey(Prefix_NeoAccount, account);
NeoAccountState? state = snapshot.TryGet(key)?.GetInteroperable<NeoAccountState>();
uint balanceHeight = state?.BalanceHeight ?? 0;
ECPoint? voteTo = state?.VoteTo;
Struct result = new()
{
balance,
balanceHeight,
voteTo is null ? StackItem.Null : voteTo.EncodePoint(true)
};
return result;
}

/// <summary>
/// Gets the first 256 registered candidates.
/// </summary>
Expand Down Expand Up @@ -463,6 +492,60 @@ async ContractTask _OnTransfer(ApplicationEngine engine, UInt160 assetId, UInt16
var list = engine.CurrentContext!.GetState<ExecutionContextState>().CallingContext!.GetState<List<GasDistribution>>();
foreach (var distribution in list)
await TokenManagement.MintInternal(engine, GasTokenId, distribution.Account, distribution.Amount, assertOwner: false, callOnBalanceChanged: false, callOnPayment: true, callOnTransfer: false);

// Handle unclaimed gas distribution when transferring zero amount
// This allows claiming unclaimed gas by transferring 0 NEO
if (amount.IsZero && from is not null)
Copy link

Choose a reason for hiding this comment

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

maybe move this piece of code to a function, @superboyiii

{
StorageKey accountKey = CreateStorageKey(Prefix_NeoAccount, from);
var accountStateItem = engine.SnapshotCache.TryGet(accountKey);
if (accountStateItem is not null)
{
accountStateItem = engine.SnapshotCache.GetAndChange(accountKey);
if (accountStateItem is not null)
{
NeoAccountState accountState = accountStateItem.GetInteroperable<NeoAccountState>();
BigInteger balance = NativeContract.TokenManagement.BalanceOf(engine.SnapshotCache, NeoTokenId, from);
GasDistribution? distribution = DistributeGas(engine, from, accountState, balance);
if (distribution is not null)
await TokenManagement.MintInternal(engine, GasTokenId, distribution.Account, distribution.Amount, assertOwner: false, callOnBalanceChanged: false, callOnPayment: true, callOnTransfer: false);
}
}
}
}

/// <summary>
/// Handles NEP-27 payment for validator registration.
/// </summary>
/// <param name="engine">The engine used to process the payment.</param>
/// <param name="assetId">The asset identifier.</param>
/// <param name="from">The sender account.</param>
/// <param name="amount">The amount of tokens sent.</param>
/// <param name="data">Optional data containing the public key for registration.</param>
[ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)]
private async ContractTask _OnPayment(ApplicationEngine engine, UInt160 assetId, UInt160 from, BigInteger amount, StackItem data)
Copy link
Owner

Choose a reason for hiding this comment

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

Do we have to keep this? RegisterCandidate doesn't work?

Copy link
Author

Choose a reason for hiding this comment

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

Both can work. Just keep it compatible. Not bad but more convenient.

{
// Only accept GAS for NEP-27 registration, not NEO
if (assetId != GasTokenId)
throw new InvalidOperationException($"Only GAS can be accepted for validator registration via NEP-27, got {assetId}");

// Check if the amount matches the registration price
long registerPrice = GetRegisterPrice(engine.SnapshotCache);
if ((long)amount != registerPrice)
throw new ArgumentOutOfRangeException(nameof(amount), $"Amount must equal the registration price {registerPrice}, got {amount}");

// Extract public key from data
if (data is not ByteString dataBytes || dataBytes.GetSpan().Length == 0)
throw new FormatException("Data parameter must contain the public key for registration");

ECPoint pubkey = ECPoint.DecodePoint(dataBytes.GetSpan(), ECCurve.Secp256r1);

// Register the candidate
if (!RegisterInternal(engine, pubkey))
throw new InvalidOperationException("Failed to register candidate. The witness does not match the public key.");
Copy link

Choose a reason for hiding this comment

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

how about if it is already registered?
should we abort?

Copy link

Choose a reason for hiding this comment

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

Maybe it is good to return false as well


// Burn the registration fee (the GAS sent to this contract)
await TokenManagement.BurnInternal(engine, GasTokenId, Hash, amount, assertOwner: false, callOnBalanceChanged: false, callOnTransfer: false);
}

GasDistribution? DistributeGas(ApplicationEngine engine, UInt160 account, NeoAccountState state, BigInteger balance)
Expand Down
10 changes: 9 additions & 1 deletion src/Neo/Wallets/AssetDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public class AssetDescriptor
/// <param name="assetId">The id of the asset.</param>
public AssetDescriptor(DataCache snapshot, ProtocolSettings settings, UInt160 assetId)
{
// GasToken is managed by TokenManagement, not a contract itself
// GasToken and NeoToken are managed by TokenManagement, not contracts themselves
Copy link
Owner

Choose a reason for hiding this comment

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

We don't need to support NEP-17 in N4 I think.

if (assetId.Equals(NativeContract.Governance.GasTokenId))
{
TokenState token = NativeContract.TokenManagement.GetTokenInfo(snapshot, assetId)!;
Expand All @@ -59,6 +59,14 @@ public AssetDescriptor(DataCache snapshot, ProtocolSettings settings, UInt160 as
Symbol = token.Symbol;
Decimals = token.Decimals;
}
else if (assetId.Equals(NativeContract.Governance.NeoTokenId))
{
TokenState token = NativeContract.TokenManagement.GetTokenInfo(snapshot, assetId)!;
AssetId = assetId;
AssetName = Governance.NeoTokenName;
Symbol = token.Symbol;
Decimals = token.Decimals;
}
else
{
var contract = NativeContract.ContractManagement.GetContract(snapshot, assetId)
Expand Down
12 changes: 6 additions & 6 deletions src/Neo/Wallets/Wallet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -565,9 +565,9 @@ public Transaction MakeTransaction(DataCache snapshot, TransferOutput[] outputs,
foreach (UInt160 account in accounts)
{
BigInteger value;
// GAS token uses TokenManagement.BalanceOf which requires assetId as first parameter
// So we can't use EmitDynamicCall with GasTokenId as contract address
if (assetId.Equals(NativeContract.Governance.GasTokenId))
// GAS and NEO tokens use TokenManagement.BalanceOf which requires assetId as first parameter
// So we can't use EmitDynamicCall with GasTokenId or NeoTokenId as contract address
if (assetId.Equals(NativeContract.Governance.GasTokenId) || assetId.Equals(NativeContract.Governance.NeoTokenId))
{
value = NativeContract.TokenManagement.BalanceOf(snapshot, assetId, account);
}
Expand Down Expand Up @@ -604,9 +604,9 @@ public Transaction MakeTransaction(DataCache snapshot, TransferOutput[] outputs,
Scopes = WitnessScope.CalledByEntry
});
}
// GAS token uses TokenManagement.Transfer which requires assetId as first parameter
// So we need to call TokenManagement contract's transfer method, not GasTokenId's transfer
if (assetId.Equals(NativeContract.Governance.GasTokenId))
// GAS and NEO tokens use TokenManagement.Transfer which requires assetId as first parameter
// So we need to call TokenManagement contract's transfer method, not GasTokenId's or NeoTokenId's transfer
if (assetId.Equals(NativeContract.Governance.GasTokenId) || assetId.Equals(NativeContract.Governance.NeoTokenId))
sb.EmitDynamicCall(NativeContract.TokenManagement.Hash, "transfer", assetId, account, output.ScriptHash, value, output.Data);
else
sb.EmitDynamicCall(output.AssetId, "transfer", account, output.ScriptHash, value, output.Data);
Expand Down
40 changes: 31 additions & 9 deletions tests/Neo.UnitTests/Extensions/UT_ContractStateExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@

using Neo.Extensions.IO;
using Neo.Extensions.SmartContract;
using Neo.SmartContract;
using Neo.SmartContract.Native;
using System.Numerics;
using System.Reflection;

namespace Neo.UnitTests.Extensions;

Expand All @@ -29,29 +32,48 @@ public void Initialize()
[TestMethod]
public void TestGetStorage()
{
var contractStorage = NativeContract.ContractManagement.FindContractStorage(_system.StoreView, NativeContract.NEO.Id);
var contractStorage = NativeContract.ContractManagement.FindContractStorage(_system.StoreView, NativeContract.Governance.Id);
Assert.IsNotNull(contractStorage);

var neoContract = NativeContract.ContractManagement.GetContractById(_system.StoreView, NativeContract.NEO.Id);
Assert.IsNotNull(neoContract);
var governanceContract = NativeContract.ContractManagement.GetContractById(_system.StoreView, NativeContract.Governance.Id);
Assert.IsNotNull(governanceContract);

contractStorage = neoContract.FindStorage(_system.StoreView);
contractStorage = governanceContract.FindStorage(_system.StoreView);

Assert.IsNotNull(contractStorage);

contractStorage = neoContract.FindStorage(_system.StoreView, [20]);
contractStorage = governanceContract.FindStorage(_system.StoreView, [10]);

Assert.IsNotNull(contractStorage);

UInt160 address = "0x9f8f056a53e39585c7bb52886418c7bed83d126b";
var item = neoContract.GetStorage(_system.StoreView, [20, .. address.ToArray()]);
var item = governanceContract.GetStorage(_system.StoreView, [10, .. address.ToArray()]);

Assert.IsNotNull(item);
Assert.AreEqual(100_000_000, item.GetInteroperable<AccountState>().Balance);
var neoAccountStateType = typeof(Governance).GetNestedType("NeoAccountState", BindingFlags.NonPublic | BindingFlags.Instance)
?? throw new InvalidOperationException("NeoAccountState type not found");
var neoAccountState = GetInteroperable(item, neoAccountStateType);
// NeoAccountState has BalanceHeight, VoteTo, and LastGasPerVote fields (not Balance)
// NEO token balance is now stored in TokenManagement, not in NeoAccountState
var balanceHeightField = neoAccountStateType.GetField("BalanceHeight", BindingFlags.Public | BindingFlags.Instance);
var balanceHeight = (uint)(balanceHeightField?.GetValue(neoAccountState) ?? throw new InvalidOperationException("BalanceHeight field not found"));
// The test address should have a BalanceHeight value from the genesis block
Assert.IsTrue(balanceHeight >= 0, "BalanceHeight should be a valid block height");

// Ensure GetInteroperableClone don't change nothing

item.GetInteroperableClone<AccountState>().Balance = 123;
Assert.AreEqual(100_000_000, item.GetInteroperable<AccountState>().Balance);
var cloneMethod = typeof(StorageItem).GetMethod("GetInteroperableClone", BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null);
var genericMethod = cloneMethod?.MakeGenericMethod(neoAccountStateType);
var clonedState = genericMethod?.Invoke(item, null);
balanceHeightField?.SetValue(clonedState, (uint)123);
var balanceHeightAfterClone = (uint)(balanceHeightField?.GetValue(neoAccountState) ?? throw new InvalidOperationException("BalanceHeight field not found"));
Assert.AreEqual(balanceHeight, balanceHeightAfterClone, "Original state should not be affected by clone modification");
}

private static object GetInteroperable(StorageItem item, Type type)
{
var method = typeof(StorageItem).GetMethod("GetInteroperable", BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null);
var genericMethod = method?.MakeGenericMethod(type);
return genericMethod?.Invoke(item, null) ?? throw new InvalidOperationException("GetInteroperable method not found");
}
}
39 changes: 0 additions & 39 deletions tests/Neo.UnitTests/Extensions/UT_NeoTokenExtensions.cs

This file was deleted.

2 changes: 1 addition & 1 deletion tests/Neo.UnitTests/GasTests/GasFixturesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public static void AssertFixture(GasTestFixture fixture, DataCache snapshot)
{
if (fixture.Signature.SignedByCommittee)
{
signatures.Add(NativeContract.NEO.GetCommitteeAddress(snapshot));
signatures.Add(NativeContract.Governance.GetCommitteeAddress(snapshot));
}
}

Expand Down
2 changes: 1 addition & 1 deletion tests/Neo.UnitTests/Ledger/UT_Blockchain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ internal static StorageKey CreateStorageKey(byte prefix, byte[]? key = null)
key?.CopyTo(buffer.AsSpan(1));
return new()
{
Id = NativeContract.NEO.Id,
Id = NativeContract.Governance.Id,
Key = buffer
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,6 @@ public void Verify()

Assert.IsFalse(test.Verify(snapshotCache, new Transaction() { Signers = Array.Empty<Signer>(), Attributes = [test], Witnesses = null! }));
Assert.IsFalse(test.Verify(snapshotCache, new Transaction() { Signers = new Signer[] { new() { Account = UInt160.Parse("0xa400ff00ff00ff00ff00ff00ff00ff00ff00ff01") } }, Attributes = [test], Witnesses = null! }));
Assert.IsTrue(test.Verify(snapshotCache, new Transaction() { Signers = new Signer[] { new() { Account = NativeContract.NEO.GetCommitteeAddress(snapshotCache) } }, Attributes = [test], Witnesses = null! }));
Assert.IsTrue(test.Verify(snapshotCache, new Transaction() { Signers = new Signer[] { new() { Account = NativeContract.Governance.GetCommitteeAddress(snapshotCache) } }, Attributes = [test], Witnesses = null! }));
}
}
6 changes: 3 additions & 3 deletions tests/Neo.UnitTests/Network/P2P/Payloads/UT_Transaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -677,7 +677,7 @@ public void FeeIsSignatureContract_TestScope_CurrentHash_NEO_FAULT()
{
Account = acc.ScriptHash,
Scopes = WitnessScope.CustomContracts,
AllowedContracts = [NativeContract.NEO.Hash]
AllowedContracts = [NativeContract.Governance.NeoTokenId]
} };

// using this...
Expand Down Expand Up @@ -738,7 +738,7 @@ public void FeeIsSignatureContract_TestScope_CurrentHash_NEO_GAS()
{
Account = acc.ScriptHash,
Scopes = WitnessScope.CustomContracts,
AllowedContracts = [NativeContract.NEO.Hash, NativeContract.TokenManagement.Hash]
AllowedContracts = [NativeContract.Governance.NeoTokenId, NativeContract.TokenManagement.Hash]
} };

// using this...
Expand Down Expand Up @@ -838,7 +838,7 @@ public void FeeIsSignatureContract_TestScope_NoScopeFAULT()
{
Account = acc.ScriptHash,
Scopes = WitnessScope.CustomContracts,
AllowedContracts = [NativeContract.NEO.Hash, NativeContract.Governance.GasTokenId]
AllowedContracts = [NativeContract.Governance.NeoTokenId, NativeContract.Governance.GasTokenId]
} };

// using this...
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ public void TestContractManifestFromJson()
public void TestEquals()
{
var descriptor1 = ContractPermissionDescriptor.CreateWildcard();
var descriptor2 = ContractPermissionDescriptor.Create(NativeContract.NEO.Hash);
var descriptor2 = ContractPermissionDescriptor.Create(NativeContract.Governance.NeoTokenId);

Assert.AreNotEqual(descriptor1, descriptor2);

var descriptor3 = ContractPermissionDescriptor.Create(NativeContract.NEO.Hash);
var descriptor3 = ContractPermissionDescriptor.Create(NativeContract.Governance.NeoTokenId);

Assert.AreEqual(descriptor2, descriptor3);
}
Expand Down
10 changes: 5 additions & 5 deletions tests/Neo.UnitTests/SmartContract/Native/UT_GasToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,11 @@ public async Task Check_BalanceOfTransferAndBurn()

// Transfer

Assert.IsTrue(NativeContract.NEO.Transfer(snapshot, from, to, BigInteger.Zero, true, persistingBlock));
Assert.ThrowsExactly<InvalidOperationException>(() => _ = NativeContract.NEO.Transfer(snapshot, from, null, BigInteger.Zero, true, persistingBlock));
Assert.ThrowsExactly<InvalidOperationException>(() => _ = NativeContract.NEO.Transfer(snapshot, null, to, BigInteger.Zero, false, persistingBlock));
Assert.AreEqual(100000000, NativeContract.NEO.BalanceOf(snapshot, from));
Assert.AreEqual(0, NativeContract.NEO.BalanceOf(snapshot, to));
Assert.IsTrue(UT_NeoToken.Transfer(snapshot, from, to, BigInteger.Zero, true, persistingBlock));
Assert.ThrowsExactly<InvalidOperationException>(() => _ = UT_NeoToken.Transfer(snapshot, from, null, BigInteger.Zero, true, persistingBlock));
Assert.ThrowsExactly<InvalidOperationException>(() => _ = UT_NeoToken.Transfer(snapshot, null, to, BigInteger.Zero, false, persistingBlock));
Assert.AreEqual(100000000, UT_NeoToken.BalanceOf(snapshot, from));
Assert.AreEqual(0, UT_NeoToken.BalanceOf(snapshot, to));

Assert.AreEqual(52000500_00000000, NativeContract.TokenManagement.BalanceOf(snapshot, NativeContract.Governance.GasTokenId, new UInt160(from)));
Assert.AreEqual(0, NativeContract.TokenManagement.BalanceOf(snapshot, NativeContract.Governance.GasTokenId, new UInt160(to)));
Expand Down
Loading