Merge pull request #2007 from nspcc-dev/fix-diff-92057
core: do not allow NEP17 roundtrip in case of insufficient funds
This commit is contained in:
commit
e89a0185ee
4 changed files with 115 additions and 5 deletions
|
@ -498,9 +498,15 @@ func initBasicChain(t *testing.T, bc *Blockchain) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func newNEP17Transfer(sc, from, to util.Uint160, amount int64, additionalArgs ...interface{}) *transaction.Transaction {
|
func newNEP17Transfer(sc, from, to util.Uint160, amount int64, additionalArgs ...interface{}) *transaction.Transaction {
|
||||||
|
return newNEP17TransferWithAssert(sc, from, to, amount, true, additionalArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNEP17TransferWithAssert(sc, from, to util.Uint160, amount int64, needAssert bool, additionalArgs ...interface{}) *transaction.Transaction {
|
||||||
w := io.NewBufBinWriter()
|
w := io.NewBufBinWriter()
|
||||||
emit.AppCall(w.BinWriter, sc, "transfer", callflag.All, from, to, amount, additionalArgs)
|
emit.AppCall(w.BinWriter, sc, "transfer", callflag.All, from, to, amount, additionalArgs)
|
||||||
emit.Opcodes(w.BinWriter, opcode.ASSERT)
|
if needAssert {
|
||||||
|
emit.Opcodes(w.BinWriter, opcode.ASSERT)
|
||||||
|
}
|
||||||
if w.Err != nil {
|
if w.Err != nil {
|
||||||
panic(fmt.Errorf("failed to create nep17 transfer transaction: %w", w.Err))
|
panic(fmt.Errorf("failed to create nep17 transfer transaction: %w", w.Err))
|
||||||
}
|
}
|
||||||
|
@ -677,7 +683,11 @@ func transferTokenFromMultisigAccountCheckOK(t *testing.T, chain *Blockchain, to
|
||||||
}
|
}
|
||||||
|
|
||||||
func transferTokenFromMultisigAccount(t *testing.T, chain *Blockchain, to, tokenHash util.Uint160, amount int64, additionalArgs ...interface{}) *transaction.Transaction {
|
func transferTokenFromMultisigAccount(t *testing.T, chain *Blockchain, to, tokenHash util.Uint160, amount int64, additionalArgs ...interface{}) *transaction.Transaction {
|
||||||
transferTx := newNEP17Transfer(tokenHash, testchain.MultisigScriptHash(), to, amount, additionalArgs...)
|
return transferTokenFromMultisigAccountWithAssert(t, chain, to, tokenHash, amount, true, additionalArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func transferTokenFromMultisigAccountWithAssert(t *testing.T, chain *Blockchain, to, tokenHash util.Uint160, amount int64, needAssert bool, additionalArgs ...interface{}) *transaction.Transaction {
|
||||||
|
transferTx := newNEP17TransferWithAssert(tokenHash, testchain.MultisigScriptHash(), to, amount, needAssert, additionalArgs...)
|
||||||
transferTx.SystemFee = 100000000
|
transferTx.SystemFee = 100000000
|
||||||
transferTx.ValidUntilBlock = chain.BlockHeight() + 1
|
transferTx.ValidUntilBlock = chain.BlockHeight() + 1
|
||||||
addSigners(neoOwner, transferTx)
|
addSigners(neoOwner, transferTx)
|
||||||
|
|
|
@ -163,7 +163,9 @@ func (c *nep17TokenNative) emitTransfer(ic *interop.Context, from, to *util.Uint
|
||||||
ic.Notifications = append(ic.Notifications, ne)
|
ic.Notifications = append(ic.Notifications, ne)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *nep17TokenNative) updateAccBalance(ic *interop.Context, acc util.Uint160, amount *big.Int) error {
|
// updateAccBalance adds specified amount to the acc's balance. If requiredBalance
|
||||||
|
// is set and amount is 0, then acc's balance is checked against requiredBalance.
|
||||||
|
func (c *nep17TokenNative) updateAccBalance(ic *interop.Context, acc util.Uint160, amount *big.Int, requiredBalance *big.Int) error {
|
||||||
key := makeAccountKey(acc)
|
key := makeAccountKey(acc)
|
||||||
si := ic.DAO.GetStorageItem(c.ID, key)
|
si := ic.DAO.GetStorageItem(c.ID, key)
|
||||||
if si == nil {
|
if si == nil {
|
||||||
|
@ -171,6 +173,24 @@ func (c *nep17TokenNative) updateAccBalance(ic *interop.Context, acc util.Uint16
|
||||||
return errors.New("insufficient funds")
|
return errors.New("insufficient funds")
|
||||||
}
|
}
|
||||||
si = state.StorageItem{}
|
si = state.StorageItem{}
|
||||||
|
} else if amount.Sign() == 0 && requiredBalance != nil {
|
||||||
|
// If amount == 0 then it's either a roundtrip or an empty transfer. In
|
||||||
|
// case of a roundtrip account's balance may still be less than actual
|
||||||
|
// transfer's amount, so we need to check it. Other cases are handled by
|
||||||
|
// `incBalance` method.
|
||||||
|
balance, err := c.balFromBytes(&si)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to deserialise balance: %w", err)
|
||||||
|
}
|
||||||
|
if balance.Cmp(requiredBalance) < 0 {
|
||||||
|
// Firstly, need to put it back to storage as it affects dumps.
|
||||||
|
err = ic.DAO.PutStorageItem(c.ID, key, si)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Finally, return an error.
|
||||||
|
return errors.New("insufficient funds")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := c.incBalance(ic, acc, &si, amount)
|
err := c.incBalance(ic, acc, &si, amount)
|
||||||
|
@ -207,12 +227,12 @@ func (c *nep17TokenNative) TransferInternal(ic *interop.Context, from, to util.U
|
||||||
} else {
|
} else {
|
||||||
inc = new(big.Int).Neg(inc)
|
inc = new(big.Int).Neg(inc)
|
||||||
}
|
}
|
||||||
if err := c.updateAccBalance(ic, from, inc); err != nil {
|
if err := c.updateAccBalance(ic, from, inc, amount); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isEmpty {
|
if !isEmpty {
|
||||||
if err := c.updateAccBalance(ic, to, amount); err != nil {
|
if err := c.updateAccBalance(ic, to, amount, nil); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,10 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neo-go/internal/random"
|
"github.com/nspcc-dev/neo-go/internal/random"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm"
|
"github.com/nspcc-dev/neo-go/pkg/vm"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -68,3 +71,48 @@ func TestGAS_Refuel(t *testing.T) {
|
||||||
require.Equal(t, vm.FaultState, aer.VMState)
|
require.Equal(t, vm.FaultState, aer.VMState)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGAS_Roundtrip(t *testing.T) {
|
||||||
|
bc := newTestChain(t)
|
||||||
|
|
||||||
|
getUtilityTokenBalance := func(bc *Blockchain, acc util.Uint160) (*big.Int, uint32) {
|
||||||
|
bs, err := bc.dao.GetNEP17Balances(acc)
|
||||||
|
if err != nil {
|
||||||
|
return big.NewInt(0), 0
|
||||||
|
}
|
||||||
|
balance := bs.Trackers[bc.contracts.GAS.ID]
|
||||||
|
return &balance.Balance, balance.LastUpdatedBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
initialBalance, _ := getUtilityTokenBalance(bc, neoOwner)
|
||||||
|
require.NotNil(t, initialBalance)
|
||||||
|
|
||||||
|
t.Run("bad: amount > initial balance", func(t *testing.T) {
|
||||||
|
tx := transferTokenFromMultisigAccountWithAssert(t, bc, neoOwner, bc.contracts.GAS.Hash, initialBalance.Int64()+1, false)
|
||||||
|
aer, err := bc.GetAppExecResults(tx.Hash(), trigger.Application)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, vm.HaltState, aer[0].VMState, aer[0].FaultException) // transfer without assert => HALT state
|
||||||
|
checkResult(t, &aer[0], stackitem.NewBool(false))
|
||||||
|
require.Len(t, aer[0].Events, 0) // failed transfer => no events
|
||||||
|
// check balance and height were not changed
|
||||||
|
updatedBalance, updatedHeight := getUtilityTokenBalance(bc, neoOwner)
|
||||||
|
initialBalance.Sub(initialBalance, big.NewInt(tx.SystemFee+tx.NetworkFee))
|
||||||
|
require.Equal(t, initialBalance, updatedBalance)
|
||||||
|
require.Equal(t, bc.BlockHeight(), updatedHeight)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("good: amount < initial balance", func(t *testing.T) {
|
||||||
|
amount := initialBalance.Int64() - 10_00000000
|
||||||
|
tx := transferTokenFromMultisigAccountWithAssert(t, bc, neoOwner, bc.contracts.GAS.Hash, amount, false)
|
||||||
|
aer, err := bc.GetAppExecResults(tx.Hash(), trigger.Application)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, vm.HaltState, aer[0].VMState, aer[0].FaultException)
|
||||||
|
checkResult(t, &aer[0], stackitem.NewBool(true))
|
||||||
|
require.Len(t, aer[0].Events, 1) // roundtrip
|
||||||
|
// check balance wasn't changed and height was updated
|
||||||
|
updatedBalance, updatedHeight := getUtilityTokenBalance(bc, neoOwner)
|
||||||
|
initialBalance.Sub(initialBalance, big.NewInt(tx.SystemFee+tx.NetworkFee))
|
||||||
|
require.Equal(t, initialBalance, updatedBalance)
|
||||||
|
require.Equal(t, bc.BlockHeight(), updatedHeight)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -325,3 +325,35 @@ func TestRegisterPrice(t *testing.T) {
|
||||||
testGetSet(t, bc, bc.contracts.NEO.Hash, "RegisterPrice",
|
testGetSet(t, bc, bc.contracts.NEO.Hash, "RegisterPrice",
|
||||||
native.DefaultRegisterPrice, 1, math.MaxInt64)
|
native.DefaultRegisterPrice, 1, math.MaxInt64)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNEO_Roundtrip(t *testing.T) {
|
||||||
|
bc := newTestChain(t)
|
||||||
|
initialBalance, initialHeight := bc.GetGoverningTokenBalance(neoOwner)
|
||||||
|
require.NotNil(t, initialBalance)
|
||||||
|
|
||||||
|
t.Run("bad: amount > initial balance", func(t *testing.T) {
|
||||||
|
tx := transferTokenFromMultisigAccountWithAssert(t, bc, neoOwner, bc.contracts.NEO.Hash, initialBalance.Int64()+1, false)
|
||||||
|
aer, err := bc.GetAppExecResults(tx.Hash(), trigger.Application)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, vm.HaltState, aer[0].VMState, aer[0].FaultException) // transfer without assert => HALT state
|
||||||
|
checkResult(t, &aer[0], stackitem.NewBool(false))
|
||||||
|
require.Len(t, aer[0].Events, 0) // failed transfer => no events
|
||||||
|
// check balance and height were not changed
|
||||||
|
updatedBalance, updatedHeight := bc.GetGoverningTokenBalance(neoOwner)
|
||||||
|
require.Equal(t, initialBalance, updatedBalance)
|
||||||
|
require.Equal(t, initialHeight, updatedHeight)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("good: amount == initial balance", func(t *testing.T) {
|
||||||
|
tx := transferTokenFromMultisigAccountWithAssert(t, bc, neoOwner, bc.contracts.NEO.Hash, initialBalance.Int64(), false)
|
||||||
|
aer, err := bc.GetAppExecResults(tx.Hash(), trigger.Application)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, vm.HaltState, aer[0].VMState, aer[0].FaultException)
|
||||||
|
checkResult(t, &aer[0], stackitem.NewBool(true))
|
||||||
|
require.Len(t, aer[0].Events, 2) // roundtrip + GAS claim
|
||||||
|
// check balance wasn't changed and height was updated
|
||||||
|
updatedBalance, updatedHeight := bc.GetGoverningTokenBalance(neoOwner)
|
||||||
|
require.Equal(t, initialBalance, updatedBalance)
|
||||||
|
require.Equal(t, bc.BlockHeight(), updatedHeight)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue