Files

484 lines
19 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package nativeasset_test
import (
"math/big"
"testing"
"github.com/holiman/uint256"
"github.com/luxfi/coreth/core/extstate"
. "github.com/luxfi/coreth/nativeasset"
"github.com/luxfi/coreth/params"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/state"
"github.com/luxfi/geth/core/tracing"
ethtypes "github.com/luxfi/geth/core/types"
"github.com/luxfi/geth/core/vm"
"github.com/stretchr/testify/assert"
// Force import core to register the VM hooks.
// This allows testing the precompiles by exercising the EVM.
_ "github.com/luxfi/coreth/core"
)
// CanTransfer checks whether there are enough funds in the address' account to make a transfer.
// This does not take the necessary gas in to account to make the transfer valid.
func CanTransfer(db vm.StateDB, addr common.Address, amount *uint256.Int) bool {
return db.GetBalance(addr).Cmp(amount) >= 0
}
// Transfer subtracts amount from sender and adds amount to recipient using the given Db
func Transfer(db vm.StateDB, sender, recipient common.Address, amount *uint256.Int) {
db.SubBalance(sender, amount, tracing.BalanceChangeTransfer)
db.AddBalance(recipient, amount, tracing.BalanceChangeTransfer)
}
func TestPackNativeAssetCallInput(t *testing.T) {
addr := common.BytesToAddress([]byte("hello"))
assetID := common.BytesToHash([]byte("ScoobyCoin"))
assetAmount := big.NewInt(50)
callData := []byte{1, 2, 3, 4, 5, 6, 7, 8}
input := PackNativeAssetCallInput(addr, assetID, assetAmount, callData)
unpackedAddr, unpackedAssetID, unpackedAssetAmount, unpackedCallData, err := UnpackNativeAssetCallInput(input)
assert.NoError(t, err)
assert.Equal(t, addr, unpackedAddr, "address")
assert.Equal(t, assetID, unpackedAssetID, "assetID")
assert.Equal(t, assetAmount, unpackedAssetAmount, "assetAmount")
assert.Equal(t, callData, unpackedCallData, "callData")
}
func TestStatefulPrecompile(t *testing.T) {
vmCtx := vm.BlockContext{
BlockNumber: big.NewInt(0),
Time: 0,
CanTransfer: CanTransfer,
Transfer: Transfer,
}
type statefulContractTest struct {
setupStateDB func() *state.StateDB
from common.Address
precompileAddr common.Address
input []byte
value *uint256.Int
gasInput uint64
expectedGasRemaining uint64
expectedErr error
expectedResult []byte
name string
stateDBCheck func(*testing.T, *state.StateDB)
}
userAddr1 := common.BytesToAddress([]byte("user1"))
userAddr2 := common.BytesToAddress([]byte("user2"))
assetID := common.BytesToHash([]byte("ScoobyCoin"))
zeroBytes := make([]byte, 32)
big0 := uint256.NewInt(0)
bigHundred := big.NewInt(100)
u256Hundred := uint256.NewInt(100)
oneHundredBytes := make([]byte, 32)
bigFifty := big.NewInt(50)
fiftyBytes := make([]byte, 32)
bigFifty.FillBytes(fiftyBytes)
bigHundred.FillBytes(oneHundredBytes)
tests := []statefulContractTest{
{
setupStateDB: func() *state.StateDB {
statedb, err := state.New(ethtypes.EmptyRootHash, state.NewDatabaseForTesting())
if err != nil {
t.Fatal(err)
}
// Create account
statedb.CreateAccount(userAddr1)
// Set balance to pay for gas fee
statedb.SetBalance(userAddr1, u256Hundred, tracing.BalanceChangeUnspecified)
// Set MultiCoin balance
statedb.Finalise(true)
return statedb
},
from: userAddr1,
precompileAddr: NativeAssetBalanceAddr,
input: PackNativeAssetBalanceInput(userAddr1, assetID),
value: big0,
gasInput: params.AssetBalanceGasCost,
expectedGasRemaining: 0,
expectedErr: nil,
expectedResult: zeroBytes,
name: "native asset balance: uninitialized multicoin balance returns 0",
},
{
setupStateDB: func() *state.StateDB {
statedb, err := state.New(ethtypes.EmptyRootHash, state.NewDatabaseForTesting())
if err != nil {
t.Fatal(err)
}
// Create account
statedb.CreateAccount(userAddr1)
// Set balance to pay for gas fee
statedb.SetBalance(userAddr1, u256Hundred, tracing.BalanceChangeUnspecified)
// Initialize multicoin balance and set it back to 0
wrappedStateDB := extstate.New(statedb)
wrappedStateDB.AddBalanceMultiCoin(userAddr1, assetID, bigHundred)
wrappedStateDB.SubBalanceMultiCoin(userAddr1, assetID, bigHundred)
wrappedStateDB.Finalise(true)
return statedb
},
from: userAddr1,
precompileAddr: NativeAssetBalanceAddr,
input: PackNativeAssetBalanceInput(userAddr1, assetID),
value: big0,
gasInput: params.AssetBalanceGasCost,
expectedGasRemaining: 0,
expectedErr: nil,
expectedResult: zeroBytes,
name: "native asset balance: initialized multicoin balance returns 0",
},
{
setupStateDB: func() *state.StateDB {
statedb, err := state.New(ethtypes.EmptyRootHash, state.NewDatabaseForTesting())
if err != nil {
t.Fatal(err)
}
// Create account
statedb.CreateAccount(userAddr1)
// Set balance to pay for gas fee
statedb.SetBalance(userAddr1, u256Hundred, tracing.BalanceChangeUnspecified)
// Initialize multicoin balance to 100
wrappedStateDB := extstate.New(statedb)
wrappedStateDB.AddBalanceMultiCoin(userAddr1, assetID, bigHundred)
wrappedStateDB.Finalise(true)
return statedb
},
from: userAddr1,
precompileAddr: NativeAssetBalanceAddr,
input: PackNativeAssetBalanceInput(userAddr1, assetID),
value: big0,
gasInput: params.AssetBalanceGasCost,
expectedGasRemaining: 0,
expectedErr: nil,
expectedResult: oneHundredBytes,
name: "native asset balance: returns correct non-zero multicoin balance",
},
{
setupStateDB: func() *state.StateDB {
statedb, err := state.New(ethtypes.EmptyRootHash, state.NewDatabaseForTesting())
if err != nil {
t.Fatal(err)
}
return statedb
},
from: userAddr1,
precompileAddr: NativeAssetBalanceAddr,
input: nil,
value: big0,
gasInput: params.AssetBalanceGasCost,
expectedGasRemaining: 0,
expectedErr: vm.ErrExecutionReverted,
expectedResult: nil,
name: "native asset balance: invalid input data reverts",
},
{
setupStateDB: func() *state.StateDB {
statedb, err := state.New(ethtypes.EmptyRootHash, state.NewDatabaseForTesting())
if err != nil {
t.Fatal(err)
}
return statedb
},
from: userAddr1,
precompileAddr: NativeAssetBalanceAddr,
input: PackNativeAssetBalanceInput(userAddr1, assetID),
value: big0,
gasInput: params.AssetBalanceGasCost - 1,
expectedGasRemaining: 0,
expectedErr: vm.ErrOutOfGas,
expectedResult: nil,
name: "native asset balance: insufficient gas errors",
},
{
setupStateDB: func() *state.StateDB {
statedb, err := state.New(ethtypes.EmptyRootHash, state.NewDatabaseForTesting())
if err != nil {
t.Fatal(err)
}
return statedb
},
from: userAddr1,
precompileAddr: NativeAssetBalanceAddr,
input: PackNativeAssetBalanceInput(userAddr1, assetID),
value: u256Hundred,
gasInput: params.AssetBalanceGasCost,
expectedGasRemaining: params.AssetBalanceGasCost,
expectedErr: vm.ErrInsufficientBalance,
expectedResult: nil,
name: "native asset balance: non-zero value with insufficient funds reverts before running pre-compile",
},
{
setupStateDB: func() *state.StateDB {
statedb, err := state.New(ethtypes.EmptyRootHash, state.NewDatabaseForTesting())
if err != nil {
t.Fatal(err)
}
statedb.SetBalance(userAddr1, u256Hundred, tracing.BalanceChangeUnspecified)
wrappedStateDB := extstate.New(statedb)
wrappedStateDB.AddBalanceMultiCoin(userAddr1, assetID, bigHundred)
wrappedStateDB.Finalise(true)
return statedb
},
from: userAddr1,
precompileAddr: NativeAssetCallAddr,
input: PackNativeAssetCallInput(userAddr2, assetID, big.NewInt(50), nil),
value: big0,
gasInput: params.AssetCallGasCost + params.CallNewAccountGas + 123,
expectedGasRemaining: 123,
expectedErr: nil,
expectedResult: nil,
name: "native asset call: multicoin transfer",
stateDBCheck: func(t *testing.T, statedb *state.StateDB) {
user1Balance := statedb.GetBalance(userAddr1)
user2Balance := statedb.GetBalance(userAddr2)
wrappedStateDB := extstate.New(statedb)
user1AssetBalance := wrappedStateDB.GetBalanceMultiCoin(userAddr1, assetID)
user2AssetBalance := wrappedStateDB.GetBalanceMultiCoin(userAddr2, assetID)
expectedBalance := big.NewInt(50)
assert.Equal(t, u256Hundred, user1Balance, "user 1 balance")
assert.Equal(t, big0, user2Balance, "user 2 balance")
assert.Equal(t, expectedBalance, user1AssetBalance, "user 1 asset balance")
assert.Equal(t, expectedBalance, user2AssetBalance, "user 2 asset balance")
},
},
{
setupStateDB: func() *state.StateDB {
statedb, err := state.New(ethtypes.EmptyRootHash, state.NewDatabaseForTesting())
if err != nil {
t.Fatal(err)
}
statedb.SetBalance(userAddr1, u256Hundred, tracing.BalanceChangeUnspecified)
wrappedStateDB := extstate.New(statedb)
wrappedStateDB.AddBalanceMultiCoin(userAddr1, assetID, bigHundred)
wrappedStateDB.Finalise(true)
return statedb
},
from: userAddr1,
precompileAddr: NativeAssetCallAddr,
input: PackNativeAssetCallInput(userAddr2, assetID, big.NewInt(50), nil),
value: uint256.NewInt(49),
gasInput: params.AssetCallGasCost + params.CallNewAccountGas,
expectedGasRemaining: 0,
expectedErr: nil,
expectedResult: nil,
name: "native asset call: multicoin transfer with non-zero value",
stateDBCheck: func(t *testing.T, statedb *state.StateDB) {
user1Balance := statedb.GetBalance(userAddr1)
user2Balance := statedb.GetBalance(userAddr2)
nativeAssetCallAddrBalance := statedb.GetBalance(NativeAssetCallAddr)
wrappedStateDB := extstate.New(statedb)
user1AssetBalance := wrappedStateDB.GetBalanceMultiCoin(userAddr1, assetID)
user2AssetBalance := wrappedStateDB.GetBalanceMultiCoin(userAddr2, assetID)
expectedBalance := big.NewInt(50)
assert.Equal(t, uint256.NewInt(51), user1Balance, "user 1 balance")
assert.Equal(t, big0, user2Balance, "user 2 balance")
assert.Equal(t, uint256.NewInt(49), nativeAssetCallAddrBalance, "native asset call addr balance")
assert.Equal(t, expectedBalance, user1AssetBalance, "user 1 asset balance")
assert.Equal(t, expectedBalance, user2AssetBalance, "user 2 asset balance")
},
},
{
setupStateDB: func() *state.StateDB {
statedb, err := state.New(ethtypes.EmptyRootHash, state.NewDatabaseForTesting())
if err != nil {
t.Fatal(err)
}
statedb.SetBalance(userAddr1, u256Hundred, tracing.BalanceChangeUnspecified)
wrappedStateDB := extstate.New(statedb)
wrappedStateDB.AddBalanceMultiCoin(userAddr1, assetID, big.NewInt(50))
wrappedStateDB.Finalise(true)
return statedb
},
from: userAddr1,
precompileAddr: NativeAssetCallAddr,
input: PackNativeAssetCallInput(userAddr2, assetID, big.NewInt(51), nil),
value: uint256.NewInt(50),
gasInput: params.AssetCallGasCost,
expectedGasRemaining: 0,
expectedErr: vm.ErrInsufficientBalance,
expectedResult: nil,
name: "native asset call: insufficient multicoin funds",
stateDBCheck: func(t *testing.T, statedb *state.StateDB) {
user1Balance := statedb.GetBalance(userAddr1)
user2Balance := statedb.GetBalance(userAddr2)
wrappedStateDB := extstate.New(statedb)
user1AssetBalance := wrappedStateDB.GetBalanceMultiCoin(userAddr1, assetID)
user2AssetBalance := wrappedStateDB.GetBalanceMultiCoin(userAddr2, assetID)
assert.Equal(t, bigHundred, user1Balance, "user 1 balance")
assert.Equal(t, big0, user2Balance, "user 2 balance")
assert.Equal(t, big.NewInt(51), user1AssetBalance, "user 1 asset balance")
assert.Equal(t, big0, user2AssetBalance, "user 2 asset balance")
},
},
{
setupStateDB: func() *state.StateDB {
statedb, err := state.New(ethtypes.EmptyRootHash, state.NewDatabaseForTesting())
if err != nil {
t.Fatal(err)
}
statedb.SetBalance(userAddr1, uint256.NewInt(50), tracing.BalanceChangeUnspecified)
wrappedStateDB := extstate.New(statedb)
wrappedStateDB.AddBalanceMultiCoin(userAddr1, assetID, big.NewInt(50))
wrappedStateDB.Finalise(true)
return statedb
},
from: userAddr1,
precompileAddr: NativeAssetCallAddr,
input: PackNativeAssetCallInput(userAddr2, assetID, big.NewInt(50), nil),
value: uint256.NewInt(51),
gasInput: params.AssetCallGasCost,
expectedGasRemaining: params.AssetCallGasCost,
expectedErr: vm.ErrInsufficientBalance,
expectedResult: nil,
name: "native asset call: insufficient funds",
stateDBCheck: func(t *testing.T, statedb *state.StateDB) {
user1Balance := statedb.GetBalance(userAddr1)
user2Balance := statedb.GetBalance(userAddr2)
wrappedStateDB := extstate.New(statedb)
user1AssetBalance := wrappedStateDB.GetBalanceMultiCoin(userAddr1, assetID)
user2AssetBalance := wrappedStateDB.GetBalanceMultiCoin(userAddr2, assetID)
assert.Equal(t, big.NewInt(50), user1Balance, "user 1 balance")
assert.Equal(t, big0, user2Balance, "user 2 balance")
assert.Equal(t, big.NewInt(50), user1AssetBalance, "user 1 asset balance")
assert.Equal(t, big0, user2AssetBalance, "user 2 asset balance")
},
},
{
setupStateDB: func() *state.StateDB {
statedb, err := state.New(ethtypes.EmptyRootHash, state.NewDatabaseForTesting())
if err != nil {
t.Fatal(err)
}
statedb.SetBalance(userAddr1, u256Hundred, tracing.BalanceChangeUnspecified)
wrappedStateDB := extstate.New(statedb)
wrappedStateDB.AddBalanceMultiCoin(userAddr1, assetID, bigHundred)
wrappedStateDB.Finalise(true)
return statedb
},
from: userAddr1,
precompileAddr: NativeAssetCallAddr,
input: PackNativeAssetCallInput(userAddr2, assetID, big.NewInt(50), nil),
value: uint256.NewInt(50),
gasInput: params.AssetCallGasCost - 1,
expectedGasRemaining: 0,
expectedErr: vm.ErrOutOfGas,
expectedResult: nil,
name: "native asset call: insufficient gas for native asset call",
},
{
setupStateDB: func() *state.StateDB {
statedb, err := state.New(ethtypes.EmptyRootHash, state.NewDatabaseForTesting())
if err != nil {
t.Fatal(err)
}
statedb.SetBalance(userAddr1, u256Hundred, tracing.BalanceChangeUnspecified)
wrappedStateDB := extstate.New(statedb)
wrappedStateDB.AddBalanceMultiCoin(userAddr1, assetID, bigHundred)
wrappedStateDB.Finalise(true)
return statedb
},
from: userAddr1,
precompileAddr: NativeAssetCallAddr,
input: PackNativeAssetCallInput(userAddr2, assetID, big.NewInt(50), nil),
value: uint256.NewInt(50),
gasInput: params.AssetCallGasCost + params.CallNewAccountGas - 1,
expectedGasRemaining: 0,
expectedErr: vm.ErrOutOfGas,
expectedResult: nil,
name: "native asset call: insufficient gas to create new account",
stateDBCheck: func(t *testing.T, statedb *state.StateDB) {
user1Balance := statedb.GetBalance(userAddr1)
user2Balance := statedb.GetBalance(userAddr2)
wrappedStateDB := extstate.New(statedb)
user1AssetBalance := wrappedStateDB.GetBalanceMultiCoin(userAddr1, assetID)
user2AssetBalance := wrappedStateDB.GetBalanceMultiCoin(userAddr2, assetID)
assert.Equal(t, bigHundred, user1Balance, "user 1 balance")
assert.Equal(t, big0, user2Balance, "user 2 balance")
assert.Equal(t, bigHundred, user1AssetBalance, "user 1 asset balance")
assert.Equal(t, big0, user2AssetBalance, "user 2 asset balance")
},
},
{
setupStateDB: func() *state.StateDB {
statedb, err := state.New(ethtypes.EmptyRootHash, state.NewDatabaseForTesting())
if err != nil {
t.Fatal(err)
}
statedb.SetBalance(userAddr1, u256Hundred, tracing.BalanceChangeUnspecified)
wrappedStateDB := extstate.New(statedb)
wrappedStateDB.AddBalanceMultiCoin(userAddr1, assetID, bigHundred)
wrappedStateDB.Finalise(true)
return statedb
},
from: userAddr1,
precompileAddr: NativeAssetCallAddr,
input: make([]byte, 24),
value: uint256.NewInt(50),
gasInput: params.AssetCallGasCost + params.CallNewAccountGas,
expectedGasRemaining: params.CallNewAccountGas,
expectedErr: vm.ErrExecutionReverted,
expectedResult: nil,
name: "native asset call: invalid input",
},
{
setupStateDB: func() *state.StateDB {
statedb, err := state.New(ethtypes.EmptyRootHash, state.NewDatabaseForTesting())
if err != nil {
t.Fatal(err)
}
statedb.SetBalance(userAddr1, u256Hundred, tracing.BalanceChangeUnspecified)
wrappedStateDB := extstate.New(statedb)
wrappedStateDB.AddBalanceMultiCoin(userAddr1, assetID, bigHundred)
wrappedStateDB.Finalise(true)
return statedb
},
from: userAddr1,
precompileAddr: GenesisContractAddr,
input: PackNativeAssetCallInput(userAddr2, assetID, big.NewInt(50), nil),
value: big0,
gasInput: params.AssetCallGasCost + params.CallNewAccountGas,
expectedGasRemaining: params.AssetCallGasCost + params.CallNewAccountGas,
expectedErr: vm.ErrExecutionReverted,
expectedResult: nil,
name: "deprecated contract",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
stateDB := test.setupStateDB()
// Create EVM with BlockNumber and Time initialized to 0.
evm := vm.NewEVM(vmCtx, stateDB, params.TestChainConfig, vm.Config{})
ret, gasRemaining, err := evm.Call(test.from, test.precompileAddr, test.input, test.gasInput, test.value)
// Place gas remaining check before error check, so that it is not skipped when there is an error
assert.Equalf(t, test.expectedGasRemaining, gasRemaining, "unexpected gas remaining (%d of %d)", gasRemaining, test.gasInput)
if test.expectedErr != nil {
assert.Equal(t, test.expectedErr, err, "expected error to match")
return
}
if assert.NoError(t, err, "EVM Call produced unexpected error") {
assert.Equal(t, test.expectedResult, ret, "unexpected return value")
if test.stateDBCheck != nil {
test.stateDBCheck(t, stateDB)
}
}
})
}
}