From 7fc57c9d587c8619d1212b5a206e2ea8987ab10d Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 10 Sep 2021 17:18:09 +0300 Subject: [PATCH] core: allow transfer 0 GAS/NEO with zero balance This commit fixes states diff at 131795 block of mainnet. Transaction: ``` NEO-GO-VM > loadbase64 DAAQDBSPsxdYh6cITC3gUKI4oWmYxJs49gwUj7MXWIenCEwt4FCiOKFpmMSbOPYUwB8MCHRyYW5zZmVyDBT1Y+pAvCg9TQ4FxI6jBbPyoHNA70FifVtSOQwAEQwUj7MXWIenCEwt4FCiOKFpmMSbOPYMFL1Mb4Fqp6gHiEwzM6xSc8fLS+RpFMAfDAh0cmFuc2ZlcgwU9WPqQLwoPU0OBcSOowWz8qBzQO9BYn1bUjk= READY: loaded 176 instructions NEO-GO-VM 0 > ops INDEX OPCODE PARAMETER 0 PUSHDATA1 ("") << 2 PUSH0 3 PUSHDATA1 8fb3175887a7084c2de050a238a16998c49b38f6 25 PUSHDATA1 8fb3175887a7084c2de050a238a16998c49b38f6 47 PUSH4 48 PACK 49 PUSH15 50 PUSHDATA1 7472616e73666572 ("transfer") 60 PUSHDATA1 f563ea40bc283d4d0e05c48ea305b3f2a07340ef // NEO token 82 SYSCALL System.Contract.Call (627d5b52) 87 ASSERT 88 PUSHDATA1 ("") 90 PUSH1 91 PUSHDATA1 8fb3175887a7084c2de050a238a16998c49b38f6 113 PUSHDATA1 bd4c6f816aa7a807884c3333ac5273c7cb4be469 135 PUSH4 136 PACK 137 PUSH15 138 PUSHDATA1 7472616e73666572 ("transfer") 148 PUSHDATA1 f563ea40bc283d4d0e05c48ea305b3f2a07340ef // NEO token 170 SYSCALL System.Contract.Call (627d5b52) 175 ASSERT ``` Go's applog: ``` { "id" : 1, "result" : { "txid" : "0x97d2ccb01467b22c73a2cb95f7af298f3a5bd8c849d7044371898b8efecdaabd", "executions" : [ { "exception" : "at instruction 87 (ASSERT): ASSERT failed", "stack" : [], "gasconsumed" : "4988995", "notifications" : [], "trigger" : "Application", "vmstate" : "FAULT" } ] }, "jsonrpc" : "2.0" } ``` C#'s applog: ``` { "jsonrpc" : "2.0", "result" : { "executions" : [ { "stack" : [], "notifications" : [ { "contract" : "0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5", "state" : { "type" : "Array", "value" : [ { "type" : "ByteString", "value" : "j7MXWIenCEwt4FCiOKFpmMSbOPY=" }, { "type" : "ByteString", "value" : "j7MXWIenCEwt4FCiOKFpmMSbOPY=" }, { "value" : "0", "type" : "Integer" } ] }, "eventname" : "Transfer" }, { "contract" : "0xd2a4cff31913016155e38e474a2c06d08be276cf", "state" : { "value" : [ { "type" : "Any" }, { "type" : "ByteString", "value" : "vUxvgWqnqAeITDMzrFJzx8tL5Gk=" }, { "value" : "2490", "type" : "Integer" } ], "type" : "Array" }, "eventname" : "Transfer" }, { "contract" : "0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5", "state" : { "value" : [ { "value" : "vUxvgWqnqAeITDMzrFJzx8tL5Gk=", "type" : "ByteString" }, { "value" : "j7MXWIenCEwt4FCiOKFpmMSbOPY=", "type" : "ByteString" }, { "value" : "1", "type" : "Integer" } ], "type" : "Array" }, "eventname" : "Transfer" } ], "vmstate" : "HALT", "gasconsumed" : "9977990", "trigger" : "Application", "exception" : null } ], "txid" : "0x97d2ccb01467b22c73a2cb95f7af298f3a5bd8c849d7044371898b8efecdaabd" }, "id" : 1 } ``` --- pkg/core/native/native_nep17.go | 6 ++- pkg/core/native_neo_test.go | 79 +++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) 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) + }) +}