Skip to content

Commit

Permalink
fix(evm): reject tx whenever a decimals conversion produces non-zero …
Browse files Browse the repository at this point in the history
…remainder
  • Loading branch information
dessaya committed Nov 7, 2023
1 parent 49309d8 commit 3bd32a9
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 77 deletions.
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

0 comments on commit 3bd32a9

Please sign in to comment.