Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(evm): reject tx whenever a decimals conversion produces non-zero remainder #3058

Merged
merged 2 commits into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 3 additions & 1 deletion packages/evm/jsonrpc/evmchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,9 @@ func (e *EVMChain) Balance(address common.Address, blockNumberOrHash *rpc.BlockN
isc.NewEthereumAddressAgentID(*e.backend.ISCChainID(), address),
*e.backend.ISCChainID(),
)
return util.BaseTokensDecimalsToEthereumDecimals(baseTokens, parameters.L1().BaseToken.Decimals), nil
ether, _ := util.BaseTokensDecimalsToEthereumDecimals(baseTokens, parameters.L1().BaseToken.Decimals)
// discard remainder
return ether, nil
}

func (e *EVMChain) Code(address common.Address, blockNumberOrHash *rpc.BlockNumberOrHash) ([]byte, error) {
Expand Down
49 changes: 37 additions & 12 deletions packages/util/decimals_hack.go
Original file line number Diff line number Diff line change
@@ -1,30 +1,55 @@
package util

import "math/big"
import (
"math/big"
)

const ethereumDecimals = uint32(18)

func adaptDecimals(value *big.Int, fromDecimals, toDecimals uint32) *big.Int {
v := new(big.Int).Set(value) // clone value
func adaptDecimals(value *big.Int, fromDecimals, toDecimals uint32) (result *big.Int, remainder *big.Int) {
result = new(big.Int)
remainder = new(big.Int)
exp := big.NewInt(10)
if toDecimals > fromDecimals {
exp.Exp(exp, big.NewInt(int64(toDecimals-fromDecimals)), nil)
return v.Mul(v, exp)
result.Mul(value, exp)
} else {
exp.Exp(exp, big.NewInt(int64(fromDecimals-toDecimals)), nil)
result.DivMod(value, exp, remainder)
}
exp.Exp(exp, big.NewInt(int64(fromDecimals-toDecimals)), nil)
return v.Div(v, exp)
return
}

// wei => base tokens
func EthereumDecimalsToBaseTokenDecimals(value *big.Int, baseTokenDecimals uint32) uint64 {
v := adaptDecimals(value, ethereumDecimals, baseTokenDecimals)
if !v.IsUint64() {
func EthereumDecimalsToBaseTokenDecimals(value *big.Int, baseTokenDecimals uint32) (result uint64, remainder *big.Int) {
r, m := adaptDecimals(value, ethereumDecimals, baseTokenDecimals)
if !r.IsUint64() {
panic("cannot convert ether value to base tokens: too large")
}
return v.Uint64()
return r.Uint64(), m
}

func MustEthereumDecimalsToBaseTokenDecimalsExact(value *big.Int, baseTokenDecimals uint32) (result uint64) {
r, m := EthereumDecimalsToBaseTokenDecimals(value, baseTokenDecimals)
if m.Sign() != 0 {
panic("cannot convert ether value to base tokens: non-exact conversion")
}
return r
}

// base tokens => wei
func BaseTokensDecimalsToEthereumDecimals(value uint64, baseTokenDecimals uint32) *big.Int {
return adaptDecimals(new(big.Int).SetUint64(value), baseTokenDecimals, ethereumDecimals)
func BaseTokensDecimalsToEthereumDecimals(value uint64, baseTokenDecimals uint32) (result *big.Int, remainder uint64) {
r, m := adaptDecimals(new(big.Int).SetUint64(value), baseTokenDecimals, ethereumDecimals)
if !m.IsUint64() {
panic("cannot convert ether value to base tokens: too large")
}
return r, m.Uint64()
}

func MustBaseTokensDecimalsToEthereumDecimalsExact(value uint64, baseTokenDecimals uint32) (result *big.Int) {
r, m := BaseTokensDecimalsToEthereumDecimals(value, baseTokenDecimals)
if m != 0 {
panic("cannot convert base tokens value to ether: non-exact conversion")
}
return r
}
38 changes: 20 additions & 18 deletions packages/util/decimals_hack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,39 +10,42 @@ import (
func TestBaseTokensDecimalsToEthereumDecimals(t *testing.T) {
value := uint64(12345678)
tests := []struct {
decimals uint32
expected string
decimals uint32
expected uint64
expectedRemainder uint64
}{
{
decimals: 6,
expected: "12345678000000000000",
expected: 12345678000000000000,
},
{
decimals: 18,
expected: "12345678",
expected: 12345678,
},
{
decimals: 20,
expected: "123456",
decimals: 20,
expected: 123456,
expectedRemainder: 78,
},
}
for _, test := range tests {
require.EqualValues(t,
test.expected,
BaseTokensDecimalsToEthereumDecimals(value, test.decimals).String(),
)
wei, rem := BaseTokensDecimalsToEthereumDecimals(value, test.decimals)
require.EqualValues(t, test.expected, wei.Uint64())
require.EqualValues(t, test.expectedRemainder, rem)
}
}

func TestEthereumDecimalsToBaseTokenDecimals(t *testing.T) {
value := uint64(123456789123456789)
tests := []struct {
decimals uint32
expected uint64
decimals uint32
expected uint64
expectedRemainder uint64
}{
{
decimals: 6,
expected: 123456, // extra decimal cases will be ignored
decimals: 6,
expected: 123456,
expectedRemainder: 789123456789,
},
{
decimals: 18,
Expand All @@ -54,9 +57,8 @@ func TestEthereumDecimalsToBaseTokenDecimals(t *testing.T) {
},
}
for _, test := range tests {
require.EqualValues(t,
test.expected,
EthereumDecimalsToBaseTokenDecimals(new(big.Int).SetUint64(value), test.decimals),
)
bt, rem := EthereumDecimalsToBaseTokenDecimals(new(big.Int).SetUint64(value), test.decimals)
require.EqualValues(t, test.expected, bt)
require.EqualValues(t, test.expectedRemainder, rem.Uint64())
}
}
20 changes: 17 additions & 3 deletions packages/vm/core/evm/emulator/statedb.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/iotaledger/wasp/packages/kv"
"github.com/iotaledger/wasp/packages/kv/codec"
"github.com/iotaledger/wasp/packages/util"
"github.com/iotaledger/wasp/packages/vm/core/errors/coreerrors"
)

const (
Expand Down Expand Up @@ -74,14 +75,20 @@ func (s *StateDB) CreateAccount(addr common.Address) {
CreateAccount(s.kv, addr)
}

var ErrNonZeroWeiRemainder = coreerrors.Register("cannot convert %d to base tokens decimals: non-zero remainder (%d)")

func (s *StateDB) SubBalance(addr common.Address, amount *big.Int) {
if amount.Sign() == 0 {
return
}
if amount.Sign() == -1 {
panic("unexpected negative amount")
}
s.ctx.SubBaseTokensBalance(addr, util.EthereumDecimalsToBaseTokenDecimals(amount, s.ctx.BaseTokensDecimals()))
baseTokens, remainder := util.EthereumDecimalsToBaseTokenDecimals(amount, s.ctx.BaseTokensDecimals())
if remainder.Sign() != 0 {
panic(ErrNonZeroWeiRemainder.Create(amount.Uint64(), remainder.Uint64()))
}
s.ctx.SubBaseTokensBalance(addr, baseTokens)
}

func (s *StateDB) AddBalance(addr common.Address, amount *big.Int) {
Expand All @@ -91,11 +98,18 @@ func (s *StateDB) AddBalance(addr common.Address, amount *big.Int) {
if amount.Sign() == -1 {
panic("unexpected negative amount")
}
s.ctx.AddBaseTokensBalance(addr, util.EthereumDecimalsToBaseTokenDecimals(amount, s.ctx.BaseTokensDecimals()))
baseTokens, remainder := util.EthereumDecimalsToBaseTokenDecimals(amount, s.ctx.BaseTokensDecimals())
if remainder.Sign() != 0 {
panic(ErrNonZeroWeiRemainder.Create(amount.Uint64(), remainder.Uint64()))
}
s.ctx.AddBaseTokensBalance(addr, baseTokens)
}

func (s *StateDB) GetBalance(addr common.Address) *big.Int {
return util.BaseTokensDecimalsToEthereumDecimals(s.ctx.GetBaseTokensBalance(addr), s.ctx.BaseTokensDecimals())
baseTokens := s.ctx.GetBaseTokensBalance(addr)
wei, _ := util.BaseTokensDecimalsToEthereumDecimals(baseTokens, s.ctx.BaseTokensDecimals())
// discard remainder
return wei
}

func GetNonce(s kv.KVStoreReader, addr common.Address) uint64 {
Expand Down
5 changes: 3 additions & 2 deletions packages/vm/core/evm/evmimpl/impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -449,15 +449,16 @@ func newL1Deposit(ctx isc.Sandbox) dict.Dict {
ctx.RequireNoError(err, "unable to parse assets from params")

// create a fake tx so that the deposit is visible by the EVM
value := util.BaseTokensDecimalsToEthereumDecimals(assets.BaseTokens, newEmulatorContext(ctx).BaseTokensDecimals())
// discard remainder in decimals conversion
wei, _ := util.BaseTokensDecimalsToEthereumDecimals(assets.BaseTokens, newEmulatorContext(ctx).BaseTokensDecimals())
nonce := uint64(0)
// encode the txdata as <AgentID sender>+<Assets>
txData := []byte{}
txData = append(txData, agentIDBytes...)
txData = append(txData, assets.Bytes()...)
chainInfo := ctx.ChainInfo()
gasPrice := chainInfo.GasFeePolicy.GasPriceWei(parameters.L1().BaseToken.Decimals)
tx := types.NewTransaction(nonce, addr, value, 0, gasPrice, txData)
tx := types.NewTransaction(nonce, addr, wei, 0, gasPrice, txData)

// create a fake receipt
receipt := &types.Receipt{
Expand Down
75 changes: 35 additions & 40 deletions packages/vm/core/evm/evmtest/evm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1539,22 +1539,12 @@ func TestEVMTransferBaseTokens(t *testing.T) {

// issue a tx with non-0 amount (try to send ETH/basetoken)
// try sending 1 million base tokens (expressed in ethereum decimals)
value := util.BaseTokensDecimalsToEthereumDecimals(
value := util.MustBaseTokensDecimalsToEthereumDecimalsExact(
1*isc.Million,
testparameters.GetL1ParamsForTesting().BaseToken.Decimals,
)
sendTx(value)
env.Chain.AssertL2BaseTokens(someAgentID, 1*isc.Million)

// by default iota/shimmer base token has 6 decimal cases, so anything past the 6th decimal case should be ignored
valueWithExtraDecimals := big.NewInt(1_000_000_999_999_999_999) // all these 9's will be ignored and only 1 million tokens should be transferred
sendTx(valueWithExtraDecimals)
env.Chain.AssertL2BaseTokens(someAgentID, 2*isc.Million)

// issue a tx with a too low amount
lowValue := big.NewInt(999_999_999_999) // all these 9's will be ignored and nothing should be transferred
sendTx(lowValue)
env.Chain.AssertL2BaseTokens(someAgentID, 2*isc.Million)
}

func TestSolidityTransferBaseTokens(t *testing.T) {
Expand All @@ -1566,7 +1556,7 @@ func TestSolidityTransferBaseTokens(t *testing.T) {
iscTest := env.deployISCTestContract(ethKey)

// try sending funds to `someEthereumAddr` by sending a "value tx" to the isc test contract
oneMillionInEthDecimals := util.BaseTokensDecimalsToEthereumDecimals(
oneMillionInEthDecimals := util.MustBaseTokensDecimalsToEthereumDecimalsExact(
1*isc.Million,
testparameters.GetL1ParamsForTesting().BaseToken.Decimals,
)
Expand All @@ -1579,7 +1569,7 @@ func TestSolidityTransferBaseTokens(t *testing.T) {
env.Chain.AssertL2BaseTokens(someEthereumAgentID, 1*isc.Million)

// attempt to send more than the contract will have available
twoMillionInEthDecimals := util.BaseTokensDecimalsToEthereumDecimals(
twoMillionInEthDecimals := util.MustBaseTokensDecimalsToEthereumDecimalsExact(
2*isc.Million,
testparameters.GetL1ParamsForTesting().BaseToken.Decimals,
)
Expand All @@ -1591,26 +1581,6 @@ func TestSolidityTransferBaseTokens(t *testing.T) {
require.Error(t, err)
env.Chain.AssertL2BaseTokens(someEthereumAgentID, 1*isc.Million)

{
// try sending a value to too high precision (anything over the 6 decimals will be ignored)
_, err = iscTest.CallFn([]ethCallOptions{{
sender: ethKey,
value: oneMillionInEthDecimals,
// wei is expressed with 18 decimal precision, iota/smr is 6, so anything in the 12 last decimal cases will be ignored
}}, "sendTo", someEthereumAddr, big.NewInt(1_000_000_999_999_999_999))
require.Error(t, err)
env.Chain.AssertL2BaseTokens(someEthereumAgentID, 1*isc.Million)
// this will fail if the (ignored) decimals are above the contract balance,
// but if we provide enough funds, the call should succeed and the extra decimals should be correctly ignored
_, err = iscTest.CallFn([]ethCallOptions{{
sender: ethKey,
value: twoMillionInEthDecimals,
// wei is expressed with 18 decimal precision, iota/smr is 6, so anything in the 12 last decimal cases will be ignored
}}, "sendTo", someEthereumAddr, big.NewInt(1_000_000_999_999_999_999))
require.NoError(t, err)
env.Chain.AssertL2BaseTokens(someEthereumAgentID, 2*isc.Million)
}

// fund the contract via a L1 wallet ISC transfer, then call `sendTo` to use those funds
l1Wallet, _ := env.Chain.Env.NewKeyPairWithFunds()
env.Chain.TransferAllowanceTo(
Expand All @@ -1619,7 +1589,7 @@ func TestSolidityTransferBaseTokens(t *testing.T) {
l1Wallet,
)

tenMillionInEthDecimals := util.BaseTokensDecimalsToEthereumDecimals(
tenMillionInEthDecimals := util.MustBaseTokensDecimalsToEthereumDecimalsExact(
10*isc.Million,
testparameters.GetL1ParamsForTesting().BaseToken.Decimals,
)
Expand All @@ -1628,7 +1598,7 @@ func TestSolidityTransferBaseTokens(t *testing.T) {
sender: ethKey,
}}, "sendTo", someEthereumAddr, tenMillionInEthDecimals)
require.NoError(t, err)
env.Chain.AssertL2BaseTokens(someEthereumAgentID, 12*isc.Million)
env.Chain.AssertL2BaseTokens(someEthereumAgentID, 11*isc.Million)

// send more than the balance
_, err = iscTest.CallFn([]ethCallOptions{{
Expand All @@ -1637,7 +1607,7 @@ func TestSolidityTransferBaseTokens(t *testing.T) {
gasLimit: 100_000, // provide a gas limit value as the estimation will fail
}}, "sendTo", someEthereumAddr, big.NewInt(0))
require.Error(t, err)
env.Chain.AssertL2BaseTokens(someEthereumAgentID, 12*isc.Million)
env.Chain.AssertL2BaseTokens(someEthereumAgentID, 11*isc.Million)
}

func TestSendEntireBalance(t *testing.T) {
Expand All @@ -1649,7 +1619,7 @@ func TestSendEntireBalance(t *testing.T) {
// send all initial
initial := env.Chain.L2BaseTokens(isc.NewEthereumAddressAgentID(env.Chain.ChainID, ethAddr))
// try sending funds to `someEthereumAddr` by sending a "value tx"
initialBalanceInEthDecimals := util.BaseTokensDecimalsToEthereumDecimals(
initialBalanceInEthDecimals := util.MustBaseTokensDecimalsToEthereumDecimalsExact(
initial,
testparameters.GetL1ParamsForTesting().BaseToken.Decimals,
)
Expand All @@ -1669,7 +1639,7 @@ func TestSendEntireBalance(t *testing.T) {
// now try sending all balance, minus the funds needed for gas
currentBalance := env.Chain.L2BaseTokens(isc.NewEthereumAddressAgentID(env.Chain.ChainID, ethAddr))

currentBalanceInEthDecimals := util.BaseTokensDecimalsToEthereumDecimals(
currentBalanceInEthDecimals := util.MustBaseTokensDecimalsToEthereumDecimalsExact(
currentBalance,
testparameters.GetL1ParamsForTesting().BaseToken.Decimals,
)
Expand All @@ -1687,7 +1657,7 @@ func TestSendEntireBalance(t *testing.T) {

gasLimit := feePolicy.GasBudgetFromTokens(tokensForGasBudget)

valueToSendInEthDecimals := util.BaseTokensDecimalsToEthereumDecimals(
valueToSendInEthDecimals := util.MustBaseTokensDecimalsToEthereumDecimalsExact(
currentBalance-tokensForGasBudget,
testparameters.GetL1ParamsForTesting().BaseToken.Decimals,
)
Expand Down Expand Up @@ -2082,7 +2052,7 @@ func TestL1DepositEVM(t *testing.T) {
require.ErrorIs(t, err, io.EOF)

require.EqualValues(t,
util.EthereumDecimalsToBaseTokenDecimals(bal, parameters.L1().BaseToken.Decimals),
util.MustEthereumDecimalsToBaseTokenDecimalsExact(bal, parameters.L1().BaseToken.Decimals),
assets.BaseTokens)

evmRec := env.Chain.EVM().TransactionReceipt(tx.Hash())
Expand All @@ -2093,3 +2063,28 @@ func TestL1DepositEVM(t *testing.T) {
expectedGas := gas.ISCGasBudgetToEVM(iscRec.GasBurned, &feePolicy.EVMGasRatio)
require.EqualValues(t, expectedGas, evmRec.GasUsed)
}

func TestDecimalsConversion(t *testing.T) {
parameters.InitL1(parameters.L1ForTesting)
env := InitEVM(t)
ethKey, _ := env.Chain.NewEthereumAccountWithL2Funds()
iscTest := env.deployISCTestContract(ethKey)

// call any function including 999999999999 wei as value (which is just 1 wei short of 1 base token)
lessThanOneSMR := new(big.Int).SetUint64(999999999999)
valueInBaseTokens, remainder := util.EthereumDecimalsToBaseTokenDecimals(
lessThanOneSMR,
parameters.L1().BaseToken.Decimals,
)
t.Log(valueInBaseTokens)
require.Zero(t, valueInBaseTokens)
require.EqualValues(t, lessThanOneSMR.Uint64(), remainder.Uint64())

_, err := iscTest.CallFn(
[]ethCallOptions{{sender: ethKey, value: lessThanOneSMR, gasLimit: 100000}},
"sendTo",
iscTest.address,
big.NewInt(0),
)
require.ErrorContains(t, err, "non-zero remainder")
}
3 changes: 2 additions & 1 deletion packages/vm/vmimpl/runreq.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package vmimpl

import (
"math"
"os"
"runtime/debug"
"time"

Expand Down Expand Up @@ -207,7 +208,7 @@ func (reqctx *requestContext) callTheContract() (*vm.RequestResult, error) {
skipRequestErr = vmexceptions.IsSkipRequestException(r)
executionErr = recoverFromExecutionError(r)
reqctx.Debugf("recovered panic from contract call: %v", executionErr)
if reqctx.vm.task.WillProduceBlock() {
if os.Getenv("DEBUG") != "" || reqctx.vm.task.WillProduceBlock() {
reqctx.Debugf(string(debug.Stack()))
}
}()
Expand Down