Skip to content

Commit 3bd32a9

Browse files
committed
fix(evm): reject tx whenever a decimals conversion produces non-zero remainder
1 parent 49309d8 commit 3bd32a9

File tree

7 files changed

+117
-77
lines changed

7 files changed

+117
-77
lines changed

packages/evm/jsonrpc/evmchain.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,9 @@ func (e *EVMChain) Balance(address common.Address, blockNumberOrHash *rpc.BlockN
309309
isc.NewEthereumAddressAgentID(*e.backend.ISCChainID(), address),
310310
*e.backend.ISCChainID(),
311311
)
312-
return util.BaseTokensDecimalsToEthereumDecimals(baseTokens, parameters.L1().BaseToken.Decimals), nil
312+
ether, _ := util.BaseTokensDecimalsToEthereumDecimals(baseTokens, parameters.L1().BaseToken.Decimals)
313+
// discard remainder
314+
return ether, nil
313315
}
314316

315317
func (e *EVMChain) Code(address common.Address, blockNumberOrHash *rpc.BlockNumberOrHash) ([]byte, error) {

packages/util/decimals_hack.go

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,55 @@
11
package util
22

3-
import "math/big"
3+
import (
4+
"math/big"
5+
)
46

57
const ethereumDecimals = uint32(18)
68

7-
func adaptDecimals(value *big.Int, fromDecimals, toDecimals uint32) *big.Int {
8-
v := new(big.Int).Set(value) // clone value
9+
func adaptDecimals(value *big.Int, fromDecimals, toDecimals uint32) (result *big.Int, remainder *big.Int) {
10+
result = new(big.Int)
11+
remainder = new(big.Int)
912
exp := big.NewInt(10)
1013
if toDecimals > fromDecimals {
1114
exp.Exp(exp, big.NewInt(int64(toDecimals-fromDecimals)), nil)
12-
return v.Mul(v, exp)
15+
result.Mul(value, exp)
16+
} else {
17+
exp.Exp(exp, big.NewInt(int64(fromDecimals-toDecimals)), nil)
18+
result.DivMod(value, exp, remainder)
1319
}
14-
exp.Exp(exp, big.NewInt(int64(fromDecimals-toDecimals)), nil)
15-
return v.Div(v, exp)
20+
return
1621
}
1722

1823
// wei => base tokens
19-
func EthereumDecimalsToBaseTokenDecimals(value *big.Int, baseTokenDecimals uint32) uint64 {
20-
v := adaptDecimals(value, ethereumDecimals, baseTokenDecimals)
21-
if !v.IsUint64() {
24+
func EthereumDecimalsToBaseTokenDecimals(value *big.Int, baseTokenDecimals uint32) (result uint64, remainder *big.Int) {
25+
r, m := adaptDecimals(value, ethereumDecimals, baseTokenDecimals)
26+
if !r.IsUint64() {
2227
panic("cannot convert ether value to base tokens: too large")
2328
}
24-
return v.Uint64()
29+
return r.Uint64(), m
30+
}
31+
32+
func MustEthereumDecimalsToBaseTokenDecimalsExact(value *big.Int, baseTokenDecimals uint32) (result uint64) {
33+
r, m := EthereumDecimalsToBaseTokenDecimals(value, baseTokenDecimals)
34+
if m.Sign() != 0 {
35+
panic("cannot convert ether value to base tokens: non-exact conversion")
36+
}
37+
return r
2538
}
2639

2740
// base tokens => wei
28-
func BaseTokensDecimalsToEthereumDecimals(value uint64, baseTokenDecimals uint32) *big.Int {
29-
return adaptDecimals(new(big.Int).SetUint64(value), baseTokenDecimals, ethereumDecimals)
41+
func BaseTokensDecimalsToEthereumDecimals(value uint64, baseTokenDecimals uint32) (result *big.Int, remainder uint64) {
42+
r, m := adaptDecimals(new(big.Int).SetUint64(value), baseTokenDecimals, ethereumDecimals)
43+
if !m.IsUint64() {
44+
panic("cannot convert ether value to base tokens: too large")
45+
}
46+
return r, m.Uint64()
47+
}
48+
49+
func MustBaseTokensDecimalsToEthereumDecimalsExact(value uint64, baseTokenDecimals uint32) (result *big.Int) {
50+
r, m := BaseTokensDecimalsToEthereumDecimals(value, baseTokenDecimals)
51+
if m != 0 {
52+
panic("cannot convert base tokens value to ether: non-exact conversion")
53+
}
54+
return r
3055
}

packages/util/decimals_hack_test.go

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,39 +10,42 @@ import (
1010
func TestBaseTokensDecimalsToEthereumDecimals(t *testing.T) {
1111
value := uint64(12345678)
1212
tests := []struct {
13-
decimals uint32
14-
expected string
13+
decimals uint32
14+
expected uint64
15+
expectedRemainder uint64
1516
}{
1617
{
1718
decimals: 6,
18-
expected: "12345678000000000000",
19+
expected: 12345678000000000000,
1920
},
2021
{
2122
decimals: 18,
22-
expected: "12345678",
23+
expected: 12345678,
2324
},
2425
{
25-
decimals: 20,
26-
expected: "123456",
26+
decimals: 20,
27+
expected: 123456,
28+
expectedRemainder: 78,
2729
},
2830
}
2931
for _, test := range tests {
30-
require.EqualValues(t,
31-
test.expected,
32-
BaseTokensDecimalsToEthereumDecimals(value, test.decimals).String(),
33-
)
32+
wei, rem := BaseTokensDecimalsToEthereumDecimals(value, test.decimals)
33+
require.EqualValues(t, test.expected, wei.Uint64())
34+
require.EqualValues(t, test.expectedRemainder, rem)
3435
}
3536
}
3637

3738
func TestEthereumDecimalsToBaseTokenDecimals(t *testing.T) {
3839
value := uint64(123456789123456789)
3940
tests := []struct {
40-
decimals uint32
41-
expected uint64
41+
decimals uint32
42+
expected uint64
43+
expectedRemainder uint64
4244
}{
4345
{
44-
decimals: 6,
45-
expected: 123456, // extra decimal cases will be ignored
46+
decimals: 6,
47+
expected: 123456,
48+
expectedRemainder: 789123456789,
4649
},
4750
{
4851
decimals: 18,
@@ -54,9 +57,8 @@ func TestEthereumDecimalsToBaseTokenDecimals(t *testing.T) {
5457
},
5558
}
5659
for _, test := range tests {
57-
require.EqualValues(t,
58-
test.expected,
59-
EthereumDecimalsToBaseTokenDecimals(new(big.Int).SetUint64(value), test.decimals),
60-
)
60+
bt, rem := EthereumDecimalsToBaseTokenDecimals(new(big.Int).SetUint64(value), test.decimals)
61+
require.EqualValues(t, test.expected, bt)
62+
require.EqualValues(t, test.expectedRemainder, rem.Uint64())
6163
}
6264
}

packages/vm/core/evm/emulator/statedb.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/iotaledger/wasp/packages/kv"
1818
"github.com/iotaledger/wasp/packages/kv/codec"
1919
"github.com/iotaledger/wasp/packages/util"
20+
"github.com/iotaledger/wasp/packages/vm/core/errors/coreerrors"
2021
)
2122

2223
const (
@@ -74,14 +75,20 @@ func (s *StateDB) CreateAccount(addr common.Address) {
7475
CreateAccount(s.kv, addr)
7576
}
7677

78+
var ErrNonZeroWeiRemainder = coreerrors.Register("cannot convert %d to base tokens decimals: non-zero remainder (%d)")
79+
7780
func (s *StateDB) SubBalance(addr common.Address, amount *big.Int) {
7881
if amount.Sign() == 0 {
7982
return
8083
}
8184
if amount.Sign() == -1 {
8285
panic("unexpected negative amount")
8386
}
84-
s.ctx.SubBaseTokensBalance(addr, util.EthereumDecimalsToBaseTokenDecimals(amount, s.ctx.BaseTokensDecimals()))
87+
baseTokens, remainder := util.EthereumDecimalsToBaseTokenDecimals(amount, s.ctx.BaseTokensDecimals())
88+
if remainder.Sign() != 0 {
89+
panic(ErrNonZeroWeiRemainder.Create(amount.Uint64(), remainder.Uint64()))
90+
}
91+
s.ctx.SubBaseTokensBalance(addr, baseTokens)
8592
}
8693

8794
func (s *StateDB) AddBalance(addr common.Address, amount *big.Int) {
@@ -91,11 +98,18 @@ func (s *StateDB) AddBalance(addr common.Address, amount *big.Int) {
9198
if amount.Sign() == -1 {
9299
panic("unexpected negative amount")
93100
}
94-
s.ctx.AddBaseTokensBalance(addr, util.EthereumDecimalsToBaseTokenDecimals(amount, s.ctx.BaseTokensDecimals()))
101+
baseTokens, remainder := util.EthereumDecimalsToBaseTokenDecimals(amount, s.ctx.BaseTokensDecimals())
102+
if remainder.Sign() != 0 {
103+
panic(ErrNonZeroWeiRemainder.Create(amount.Uint64(), remainder.Uint64()))
104+
}
105+
s.ctx.AddBaseTokensBalance(addr, baseTokens)
95106
}
96107

97108
func (s *StateDB) GetBalance(addr common.Address) *big.Int {
98-
return util.BaseTokensDecimalsToEthereumDecimals(s.ctx.GetBaseTokensBalance(addr), s.ctx.BaseTokensDecimals())
109+
baseTokens := s.ctx.GetBaseTokensBalance(addr)
110+
wei, _ := util.BaseTokensDecimalsToEthereumDecimals(baseTokens, s.ctx.BaseTokensDecimals())
111+
// discard remainder
112+
return wei
99113
}
100114

101115
func GetNonce(s kv.KVStoreReader, addr common.Address) uint64 {

packages/vm/core/evm/evmimpl/impl.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -449,15 +449,16 @@ func newL1Deposit(ctx isc.Sandbox) dict.Dict {
449449
ctx.RequireNoError(err, "unable to parse assets from params")
450450

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

462463
// create a fake receipt
463464
receipt := &types.Receipt{

packages/vm/core/evm/evmtest/evm_test.go

Lines changed: 35 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1539,22 +1539,12 @@ func TestEVMTransferBaseTokens(t *testing.T) {
15391539

15401540
// issue a tx with non-0 amount (try to send ETH/basetoken)
15411541
// try sending 1 million base tokens (expressed in ethereum decimals)
1542-
value := util.BaseTokensDecimalsToEthereumDecimals(
1542+
value := util.MustBaseTokensDecimalsToEthereumDecimalsExact(
15431543
1*isc.Million,
15441544
testparameters.GetL1ParamsForTesting().BaseToken.Decimals,
15451545
)
15461546
sendTx(value)
15471547
env.Chain.AssertL2BaseTokens(someAgentID, 1*isc.Million)
1548-
1549-
// by default iota/shimmer base token has 6 decimal cases, so anything past the 6th decimal case should be ignored
1550-
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
1551-
sendTx(valueWithExtraDecimals)
1552-
env.Chain.AssertL2BaseTokens(someAgentID, 2*isc.Million)
1553-
1554-
// issue a tx with a too low amount
1555-
lowValue := big.NewInt(999_999_999_999) // all these 9's will be ignored and nothing should be transferred
1556-
sendTx(lowValue)
1557-
env.Chain.AssertL2BaseTokens(someAgentID, 2*isc.Million)
15581548
}
15591549

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

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

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

1594-
{
1595-
// try sending a value to too high precision (anything over the 6 decimals will be ignored)
1596-
_, err = iscTest.CallFn([]ethCallOptions{{
1597-
sender: ethKey,
1598-
value: oneMillionInEthDecimals,
1599-
// wei is expressed with 18 decimal precision, iota/smr is 6, so anything in the 12 last decimal cases will be ignored
1600-
}}, "sendTo", someEthereumAddr, big.NewInt(1_000_000_999_999_999_999))
1601-
require.Error(t, err)
1602-
env.Chain.AssertL2BaseTokens(someEthereumAgentID, 1*isc.Million)
1603-
// this will fail if the (ignored) decimals are above the contract balance,
1604-
// but if we provide enough funds, the call should succeed and the extra decimals should be correctly ignored
1605-
_, err = iscTest.CallFn([]ethCallOptions{{
1606-
sender: ethKey,
1607-
value: twoMillionInEthDecimals,
1608-
// wei is expressed with 18 decimal precision, iota/smr is 6, so anything in the 12 last decimal cases will be ignored
1609-
}}, "sendTo", someEthereumAddr, big.NewInt(1_000_000_999_999_999_999))
1610-
require.NoError(t, err)
1611-
env.Chain.AssertL2BaseTokens(someEthereumAgentID, 2*isc.Million)
1612-
}
1613-
16141584
// fund the contract via a L1 wallet ISC transfer, then call `sendTo` to use those funds
16151585
l1Wallet, _ := env.Chain.Env.NewKeyPairWithFunds()
16161586
env.Chain.TransferAllowanceTo(
@@ -1619,7 +1589,7 @@ func TestSolidityTransferBaseTokens(t *testing.T) {
16191589
l1Wallet,
16201590
)
16211591

1622-
tenMillionInEthDecimals := util.BaseTokensDecimalsToEthereumDecimals(
1592+
tenMillionInEthDecimals := util.MustBaseTokensDecimalsToEthereumDecimalsExact(
16231593
10*isc.Million,
16241594
testparameters.GetL1ParamsForTesting().BaseToken.Decimals,
16251595
)
@@ -1628,7 +1598,7 @@ func TestSolidityTransferBaseTokens(t *testing.T) {
16281598
sender: ethKey,
16291599
}}, "sendTo", someEthereumAddr, tenMillionInEthDecimals)
16301600
require.NoError(t, err)
1631-
env.Chain.AssertL2BaseTokens(someEthereumAgentID, 12*isc.Million)
1601+
env.Chain.AssertL2BaseTokens(someEthereumAgentID, 11*isc.Million)
16321602

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

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

1672-
currentBalanceInEthDecimals := util.BaseTokensDecimalsToEthereumDecimals(
1642+
currentBalanceInEthDecimals := util.MustBaseTokensDecimalsToEthereumDecimalsExact(
16731643
currentBalance,
16741644
testparameters.GetL1ParamsForTesting().BaseToken.Decimals,
16751645
)
@@ -1687,7 +1657,7 @@ func TestSendEntireBalance(t *testing.T) {
16871657

16881658
gasLimit := feePolicy.GasBudgetFromTokens(tokensForGasBudget)
16891659

1690-
valueToSendInEthDecimals := util.BaseTokensDecimalsToEthereumDecimals(
1660+
valueToSendInEthDecimals := util.MustBaseTokensDecimalsToEthereumDecimalsExact(
16911661
currentBalance-tokensForGasBudget,
16921662
testparameters.GetL1ParamsForTesting().BaseToken.Decimals,
16931663
)
@@ -2082,7 +2052,7 @@ func TestL1DepositEVM(t *testing.T) {
20822052
require.ErrorIs(t, err, io.EOF)
20832053

20842054
require.EqualValues(t,
2085-
util.EthereumDecimalsToBaseTokenDecimals(bal, parameters.L1().BaseToken.Decimals),
2055+
util.MustEthereumDecimalsToBaseTokenDecimalsExact(bal, parameters.L1().BaseToken.Decimals),
20862056
assets.BaseTokens)
20872057

20882058
evmRec := env.Chain.EVM().TransactionReceipt(tx.Hash())
@@ -2093,3 +2063,28 @@ func TestL1DepositEVM(t *testing.T) {
20932063
expectedGas := gas.ISCGasBudgetToEVM(iscRec.GasBurned, &feePolicy.EVMGasRatio)
20942064
require.EqualValues(t, expectedGas, evmRec.GasUsed)
20952065
}
2066+
2067+
func TestDecimalsConversion(t *testing.T) {
2068+
parameters.InitL1(parameters.L1ForTesting)
2069+
env := InitEVM(t)
2070+
ethKey, _ := env.Chain.NewEthereumAccountWithL2Funds()
2071+
iscTest := env.deployISCTestContract(ethKey)
2072+
2073+
// call any function including 999999999999 wei as value (which is just 1 wei short of 1 base token)
2074+
lessThanOneSMR := new(big.Int).SetUint64(999999999999)
2075+
valueInBaseTokens, remainder := util.EthereumDecimalsToBaseTokenDecimals(
2076+
lessThanOneSMR,
2077+
parameters.L1().BaseToken.Decimals,
2078+
)
2079+
t.Log(valueInBaseTokens)
2080+
require.Zero(t, valueInBaseTokens)
2081+
require.EqualValues(t, lessThanOneSMR.Uint64(), remainder.Uint64())
2082+
2083+
_, err := iscTest.CallFn(
2084+
[]ethCallOptions{{sender: ethKey, value: lessThanOneSMR, gasLimit: 100000}},
2085+
"sendTo",
2086+
iscTest.address,
2087+
big.NewInt(0),
2088+
)
2089+
require.ErrorContains(t, err, "non-zero remainder")
2090+
}

packages/vm/vmimpl/runreq.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package vmimpl
22

33
import (
44
"math"
5+
"os"
56
"runtime/debug"
67
"time"
78

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

0 commit comments

Comments
 (0)