diff --git a/pkg/core/native/native_nep17.go b/pkg/core/native/native_nep17.go index 3b2695fa8..8a7aa4606 100644 --- a/pkg/core/native/native_nep17.go +++ b/pkg/core/native/native_nep17.go @@ -171,9 +171,13 @@ func (c *nep17TokenNative) updateAccBalance(ic *interop.Context, acc util.Uint16 key := makeAccountKey(acc) si := ic.DAO.GetStorageItem(c.ID, key) if si == nil { - if amount.Sign() <= 0 { + if amount.Sign() < 0 { return errors.New("insufficient funds") } + if amount.Sign() == 0 { + // it's OK to transfer 0 if the balance 0, no need to put si to the storage + return nil + } si = state.StorageItem{} } diff --git a/pkg/core/native_neo_test.go b/pkg/core/native_neo_test.go index 6db7d0eba..089543113 100644 --- a/pkg/core/native_neo_test.go +++ b/pkg/core/native_neo_test.go @@ -6,6 +6,7 @@ import ( "sort" "testing" + "github.com/nspcc-dev/neo-go/internal/random" "github.com/nspcc-dev/neo-go/internal/testchain" "github.com/nspcc-dev/neo-go/pkg/config/netmode" "github.com/nspcc-dev/neo-go/pkg/core/native" @@ -357,3 +358,81 @@ func TestNEO_Roundtrip(t *testing.T) { require.Equal(t, bc.BlockHeight(), updatedHeight) }) } + +func TestNEO_TransferZeroWithZeroBalance(t *testing.T) { + bc := newTestChain(t) + + check := func(t *testing.T, roundtrip bool) { + acc := newAccountWithGAS(t, bc) + from := acc.PrivateKey().GetScriptHash() + to := from + if !roundtrip { + to = random.Uint160() + } + transferTx := newNEP17TransferWithAssert(bc.contracts.NEO.Hash, acc.PrivateKey().GetScriptHash(), to, 0, true) + transferTx.SystemFee = 100000000 + transferTx.NetworkFee = 10000000 + transferTx.ValidUntilBlock = bc.BlockHeight() + 1 + addSigners(acc.PrivateKey().GetScriptHash(), transferTx) + require.NoError(t, acc.SignTx(bc.config.Magic, transferTx)) + b := bc.newBlock(transferTx) + require.NoError(t, bc.AddBlock(b)) + + aer, err := bc.GetAppExecResults(transferTx.Hash(), trigger.Application) + require.NoError(t, err) + require.Equal(t, vm.HaltState, aer[0].VMState, aer[0].FaultException) + require.Len(t, aer[0].Events, 1) // roundtrip only, no GAS claim + require.Equal(t, stackitem.NewBigInteger(big.NewInt(0)), aer[0].Events[0].Item.Value().([]stackitem.Item)[2]) // amount is 0 + // check balance wasn't changed and height wasn't updated + updatedBalance, updatedHeight := bc.GetGoverningTokenBalance(acc.PrivateKey().GetScriptHash()) + require.Equal(t, big.NewInt(0), updatedBalance) + require.Equal(t, uint32(0), updatedHeight) + } + t.Run("roundtrip: amount == initial balance == 0", func(t *testing.T) { + check(t, true) + }) + t.Run("non-roundtrip: amount == initial balance == 0", func(t *testing.T) { + check(t, false) + }) +} + +func TestNEO_TransferZeroWithNonZeroBalance(t *testing.T) { + bc := newTestChain(t) + + check := func(t *testing.T, roundtrip bool) { + acc := newAccountWithGAS(t, bc) + transferTokenFromMultisigAccount(t, bc, acc.PrivateKey().GetScriptHash(), bc.contracts.NEO.Hash, 100) + initialBalance, _ := bc.GetGoverningTokenBalance(acc.PrivateKey().GetScriptHash()) + require.True(t, initialBalance.Sign() > 0) + + from := acc.PrivateKey().GetScriptHash() + to := from + if !roundtrip { + to = random.Uint160() + } + transferTx := newNEP17TransferWithAssert(bc.contracts.NEO.Hash, acc.PrivateKey().GetScriptHash(), to, 0, true) + transferTx.SystemFee = 100000000 + transferTx.NetworkFee = 10000000 + transferTx.ValidUntilBlock = bc.BlockHeight() + 1 + addSigners(acc.PrivateKey().GetScriptHash(), transferTx) + require.NoError(t, acc.SignTx(bc.config.Magic, transferTx)) + b := bc.newBlock(transferTx) + require.NoError(t, bc.AddBlock(b)) + + aer, err := bc.GetAppExecResults(transferTx.Hash(), trigger.Application) + require.NoError(t, err) + require.Equal(t, vm.HaltState, aer[0].VMState, aer[0].FaultException) + require.Len(t, aer[0].Events, 2) // roundtrip + GAS claim + require.Equal(t, stackitem.NewBigInteger(big.NewInt(0)), aer[0].Events[1].Item.Value().([]stackitem.Item)[2]) // amount is 0 + // check balance wasn't changed and height was updated + updatedBalance, updatedHeight := bc.GetGoverningTokenBalance(acc.PrivateKey().GetScriptHash()) + require.Equal(t, initialBalance, updatedBalance) + require.Equal(t, bc.BlockHeight(), updatedHeight) + } + t.Run("roundtrip", func(t *testing.T) { + check(t, true) + }) + t.Run("non-roundtrip", func(t *testing.T) { + check(t, false) + }) +}