Skip to content

Commit

Permalink
Merge pull request #3242 from jorgemmsilva/fix/decimals-precision-loss
Browse files Browse the repository at this point in the history
fix: decimals loss of precision on moveBetweenAccounts
  • Loading branch information
jorgemmsilva authored Jan 25, 2024
2 parents 0d48ef5 + a9d71b6 commit 01f56f2
Show file tree
Hide file tree
Showing 9 changed files with 72 additions and 127 deletions.
5 changes: 5 additions & 0 deletions packages/evm/jsonrpc/jsonrpctest/jsonrpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ func TestRPCGetBalance(t *testing.T) {
)

// 18 decimals
initialBalance := env.Balance(nonEmptyAddress)
toSend := new(big.Int).SetUint64(1_111_111_111_111_111_111) // use all 18 decimals
tx, err := types.SignTx(
types.NewTransaction(0, emptyAddress, toSend, uint64(100_000), env.MustGetGasPrice(), []byte{}),
Expand All @@ -102,6 +103,10 @@ func TestRPCGetBalance(t *testing.T) {
require.NoError(t, err)
receipt := env.mustSendTransactionAndWait(tx)
require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status)
fee := new(big.Int).Mul(receipt.EffectiveGasPrice, new(big.Int).SetUint64(receipt.GasUsed))
exptectedBalance := new(big.Int).Sub(initialBalance, toSend)
exptectedBalance = new(big.Int).Sub(exptectedBalance, fee)
require.Equal(t, exptectedBalance, env.Balance(nonEmptyAddress))
require.Equal(t, toSend, env.Balance(emptyAddress))
}

Expand Down
48 changes: 48 additions & 0 deletions packages/solo/checkledger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package solo

import (
"fmt"
"math/big"

"github.com/iotaledger/wasp/packages/isc"
"github.com/iotaledger/wasp/packages/kv"
"github.com/iotaledger/wasp/packages/kv/subrealm"
"github.com/iotaledger/wasp/packages/parameters"
"github.com/iotaledger/wasp/packages/util"
"github.com/iotaledger/wasp/packages/vm/core/accounts"
)

// only used in internal tests and solo
func CheckLedger(v isc.SchemaVersion, store kv.KVStoreReader, checkpoint string) {
state := subrealm.NewReadOnly(store, kv.Key(accounts.Contract.Hname().Bytes()))
t := accounts.GetTotalL2FungibleTokens(v, state)
c := calcL2TotalFungibleTokens(v, state)
if !t.Equals(c) {
panic(fmt.Sprintf("inconsistent on-chain account ledger @ checkpoint '%s'\n total assets: %s\ncalc total: %s\n",
checkpoint, t, c))
}
}

func calcL2TotalFungibleTokens(v isc.SchemaVersion, state kv.KVStoreReader) *isc.Assets {
ret := isc.NewEmptyAssets()
totalBaseTokens := big.NewInt(0)

accounts.AllAccountsMapR(state).IterateKeys(func(accountKey []byte) bool {
// add all native tokens owned by each account
accounts.NativeTokensMapR(state, kv.Key(accountKey)).Iterate(func(idBytes []byte, val []byte) bool {
ret.AddNativeTokens(
isc.MustNativeTokenIDFromBytes(idBytes),
new(big.Int).SetBytes(val),
)
return true
})
// use the full decimals for each account, so no dust balance is lost in the calculation
baseTokensFullDecimals := accounts.GetBaseTokensFullDecimals(v)(state, kv.Key(accountKey))
totalBaseTokens = new(big.Int).Add(totalBaseTokens, baseTokensFullDecimals)
return true
})

// convert from 18 decimals, remainder must be 0
ret.BaseTokens = util.MustEthereumDecimalsToBaseTokenDecimalsExact(totalBaseTokens, parameters.L1().BaseToken.Decimals)
return ret
}
70 changes: 0 additions & 70 deletions packages/solo/utils.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,8 @@
package solo

import (
"fmt"
"math/big"

"github.com/samber/lo"

iotago "github.com/iotaledger/iota.go/v3"
"github.com/iotaledger/wasp/packages/cryptolib"
"github.com/iotaledger/wasp/packages/isc"
"github.com/iotaledger/wasp/packages/kv"
"github.com/iotaledger/wasp/packages/parameters"
"github.com/iotaledger/wasp/packages/util"
"github.com/iotaledger/wasp/packages/vm/core/accounts"
"github.com/iotaledger/wasp/packages/vm/core/root"
)

Expand Down Expand Up @@ -54,63 +44,3 @@ func ISCRequestFromCallParams(ch *Chain, req *CallParams, keyPair *cryptolib.Key
}
return requestsFromSignedTx[ch.ChainID][0], nil
}

// only used in internal tests and solo
func CheckLedger(v isc.SchemaVersion, state kv.KVStoreReader, checkpoint string) {
t := accounts.GetTotalL2FungibleTokens(v, state)
c := calcL2TotalFungibleTokens(v, state)
if !t.Equals(c) {
panic(fmt.Sprintf("inconsistent on-chain account ledger @ checkpoint '%s'\n total assets: %s\ncalc total: %s\n",
checkpoint, t, c))
}

totalAccNFTs := accounts.GetTotalL2NFTs(state)
if len(lo.FindDuplicates(totalAccNFTs)) != 0 {
panic(fmt.Sprintf("inconsistent on-chain account ledger @ checkpoint '%s'\n duplicate NFTs\n", checkpoint))
}
calculatedNFTs := calcL2TotalNFTs(state)
if len(lo.FindDuplicates(calculatedNFTs)) != 0 {
panic(fmt.Sprintf("inconsistent on-chain account ledger @ checkpoint '%s'\n duplicate NFTs\n", checkpoint))
}
left, right := lo.Difference(calculatedNFTs, totalAccNFTs)
if len(left)+len(right) != 0 {
panic(fmt.Sprintf("inconsistent on-chain account ledger @ checkpoint '%s'\n NFTs don't match\n", checkpoint))
}
}

func calcL2TotalFungibleTokens(v isc.SchemaVersion, state kv.KVStoreReader) *isc.Assets {
ret := isc.NewEmptyAssets()
totalBaseTokens := big.NewInt(0)

accounts.AllAccountsMapR(state).IterateKeys(func(accountKey []byte) bool {
// add all native tokens owned by each account
accounts.NativeTokensMapR(state, kv.Key(accountKey)).Iterate(func(idBytes []byte, val []byte) bool {
ret.AddNativeTokens(
isc.MustNativeTokenIDFromBytes(idBytes),
new(big.Int).SetBytes(val),
)
return true
})
// use the full decimals for each account, so no dust balance is lost in the calculation
baseTokensFullDecimals := accounts.GetBaseTokensFullDecimals(v)(state, kv.Key(accountKey))
totalBaseTokens = new(big.Int).Add(totalBaseTokens, baseTokensFullDecimals)
return true
})

// convert from 18 decimals, remainder must be 0
ret.BaseTokens = util.MustEthereumDecimalsToBaseTokenDecimalsExact(totalBaseTokens, parameters.L1().BaseToken.Decimals)
return ret
}

func calcL2TotalNFTs(state kv.KVStoreReader) []iotago.NFTID {
var ret []iotago.NFTID
accounts.AllAccountsMapR(state).IterateKeys(func(key []byte) bool {
agentID, err := isc.AgentIDFromBytes(key) // obs: this can only be done because the key saves the entire bytes of agentID, unlike the BaseTokens/NativeTokens accounting
if err != nil {
panic(fmt.Errorf("calcL2TotalNFTs: %w", err))
}
ret = append(ret, accounts.GetAccountNFTs(state, agentID)...)
return true
})
return ret
}
16 changes: 0 additions & 16 deletions packages/vm/core/accounts/basetokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (

type (
getBaseTokensFn func(state kv.KVStoreReader, accountKey kv.Key) uint64
setBaseTokensFn func(state kv.KVStore, accountKey kv.Key, amount uint64)
GetBaseTokensFullDecimalsFn func(state kv.KVStoreReader, accountKey kv.Key) *big.Int
setBaseTokensFullDecimalsFn func(state kv.KVStore, accountKey kv.Key, amount *big.Int)
)
Expand All @@ -26,15 +25,6 @@ func getBaseTokens(v isc.SchemaVersion) getBaseTokensFn {
}
}

func setBaseTokens(v isc.SchemaVersion) setBaseTokensFn {
switch v {
case 0:
return setBaseTokensDEPRECATED
default:
return setBaseTokensNEW
}
}

func GetBaseTokensFullDecimals(v isc.SchemaVersion) GetBaseTokensFullDecimalsFn {
switch v {
case 0:
Expand Down Expand Up @@ -74,12 +64,6 @@ func getBaseTokensNEW(state kv.KVStoreReader, accountKey kv.Key) uint64 {
return convertedAmount
}

func setBaseTokensNEW(state kv.KVStore, accountKey kv.Key, amount uint64) {
// convert to 18 decimals
amountConverted := util.MustBaseTokensDecimalsToEthereumDecimalsExact(amount, parameters.L1().BaseToken.Decimals)
state.Set(BaseTokensKey(accountKey), codec.EncodeBigIntAbs(amountConverted))
}

func AdjustAccountBaseTokens(v isc.SchemaVersion, state kv.KVStore, account isc.AgentID, adjustment int64, chainID isc.ChainID) {
switch {
case adjustment > 0:
Expand Down
4 changes: 0 additions & 4 deletions packages/vm/core/accounts/basetokens_deprecated.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@ func getBaseTokensDEPRECATED(state kv.KVStoreReader, accountKey kv.Key) uint64 {
return codec.MustDecodeUint64(state.Get(BaseTokensKey(accountKey)), 0)
}

func setBaseTokensDEPRECATED(state kv.KVStore, accountKey kv.Key, amount uint64) {
state.Set(BaseTokensKey(accountKey), codec.EncodeUint64(amount))
}

func getBaseTokensFullDecimalsDEPRECATED(state kv.KVStoreReader, accountKey kv.Key) *big.Int {
amount := codec.MustDecodeUint64(state.Get(BaseTokensKey(accountKey)), 0)
baseTokens, _ := util.BaseTokensDecimalsToEthereumDecimals(amount, parameters.L1().BaseToken.Decimals)
Expand Down
21 changes: 13 additions & 8 deletions packages/vm/core/accounts/fungibletokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/iotaledger/wasp/packages/isc"
"github.com/iotaledger/wasp/packages/kv"
"github.com/iotaledger/wasp/packages/kv/dict"
"github.com/iotaledger/wasp/packages/parameters"
"github.com/iotaledger/wasp/packages/util"
)

Expand All @@ -29,7 +30,8 @@ func creditToAccount(v isc.SchemaVersion, state kv.KVStore, accountKey kv.Key, a
}

if assets.BaseTokens > 0 {
setBaseTokens(v)(state, accountKey, getBaseTokens(v)(state, accountKey)+assets.BaseTokens)
incomingTokensFullDecimals := util.MustBaseTokensDecimalsToEthereumDecimalsExact(assets.BaseTokens, parameters.L1().BaseToken.Decimals)
creditToAccountFullDecimals(v, state, accountKey, incomingTokensFullDecimals)
}
for _, nt := range assets.NativeTokens {
if nt.Amount.Sign() == 0 {
Expand Down Expand Up @@ -85,16 +87,19 @@ func debitFromAccount(v isc.SchemaVersion, state kv.KVStore, accountKey kv.Key,

// first check, then mutate
mutateBaseTokens := false
mutations := isc.NewEmptyAssets()

baseTokensToDebit := util.MustBaseTokensDecimalsToEthereumDecimalsExact(assets.BaseTokens, parameters.L1().BaseToken.Decimals)
var baseTokensToSet *big.Int
if assets.BaseTokens > 0 {
balance := getBaseTokens(v)(state, accountKey)
if assets.BaseTokens > balance {
balance := GetBaseTokensFullDecimals(v)(state, accountKey)
if baseTokensToDebit.Cmp(balance) > 0 {
return false
}
mutateBaseTokens = true
mutations.BaseTokens = balance - assets.BaseTokens
baseTokensToSet = new(big.Int).Sub(balance, baseTokensToDebit)
}

nativeTokensMutations := isc.NewEmptyAssets()
for _, nt := range assets.NativeTokens {
if nt.Amount.Sign() == 0 {
continue
Expand All @@ -107,13 +112,13 @@ func debitFromAccount(v isc.SchemaVersion, state kv.KVStore, accountKey kv.Key,
if balance.Sign() < 0 {
return false
}
mutations.AddNativeTokens(nt.ID, balance)
nativeTokensMutations.AddNativeTokens(nt.ID, balance)
}

if mutateBaseTokens {
setBaseTokens(v)(state, accountKey, mutations.BaseTokens)
setBaseTokensFullDecimals(v)(state, accountKey, baseTokensToSet)
}
for _, nt := range mutations.NativeTokens {
for _, nt := range nativeTokensMutations.NativeTokens {
setNativeTokenAmount(state, accountKey, nt.ID, nt.Amount)
}
return true
Expand Down
12 changes: 6 additions & 6 deletions packages/vm/core/accounts/nfts.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ func accountToNFTsMap(state kv.KVStore, agentID isc.AgentID) *collections.Map {
return collections.NewMap(state, nftsMapKey(agentID))
}

func NFTToOwnerMap(state kv.KVStore) *collections.Map {
func nftToOwnerMap(state kv.KVStore) *collections.Map {
return collections.NewMap(state, keyNFTOwner)
}

func NFTToOwnerMapR(state kv.KVStoreReader) *collections.ImmutableMap {
func nftToOwnerMapR(state kv.KVStoreReader) *collections.ImmutableMap {
return collections.NewMapReadOnly(state, keyNFTOwner)
}

Expand Down Expand Up @@ -67,7 +67,7 @@ func hasNFT(state kv.KVStoreReader, agentID isc.AgentID, nftID iotago.NFTID) boo

func removeNFTOwner(state kv.KVStore, nftID iotago.NFTID, agentID isc.AgentID) bool {
// remove the mapping of NFTID => owner
nftMap := NFTToOwnerMap(state)
nftMap := nftToOwnerMap(state)
if !nftMap.HasAt(nftID[:]) {
return false
}
Expand All @@ -84,7 +84,7 @@ func removeNFTOwner(state kv.KVStore, nftID iotago.NFTID, agentID isc.AgentID) b

func setNFTOwner(state kv.KVStore, nftID iotago.NFTID, agentID isc.AgentID) {
// add to the mapping of NFTID => owner
nftMap := NFTToOwnerMap(state)
nftMap := nftToOwnerMap(state)
nftMap.SetAt(nftID[:], agentID.Bytes())

// add to the mapping of agentID => []NFTIDs
Expand All @@ -97,7 +97,7 @@ func GetNFTData(state kv.KVStoreReader, nftID iotago.NFTID) *isc.NFT {
if o == nil {
return nil
}
owner, err := isc.AgentIDFromBytes(NFTToOwnerMapR(state).GetAt(nftID[:]))
owner, err := isc.AgentIDFromBytes(nftToOwnerMapR(state).GetAt(nftID[:]))
if err != nil {
panic("error parsing AgentID in NFTToOwnerMap")
}
Expand Down Expand Up @@ -184,7 +184,7 @@ func getAccountNFTsInCollection(state kv.KVStoreReader, agentID isc.AgentID, col
}

func getL2TotalNFTs(state kv.KVStoreReader) []iotago.NFTID {
return collectNFTIDs(NFTToOwnerMapR(state))
return collectNFTIDs(nftToOwnerMapR(state))
}

// GetAccountNFTs returns all NFTs belonging to the agentID on the state
Expand Down
8 changes: 0 additions & 8 deletions packages/vm/vmimpl/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,6 @@ import (
func (vmctx *vmContext) runMigrations(chainState kv.KVStore, migrationScheme *migrations.MigrationScheme) {
latestSchemaVersion := migrationScheme.LatestSchemaVersion()

if vmctx.task.AnchorOutput.StateIndex == 0 {
// initializing new chain -- set the schema to latest version
withContractState(chainState, root.Contract, func(s kv.KVStore) {
root.SetSchemaVersion(s, latestSchemaVersion)
})
return
}

currentVersion := root.NewStateAccess(chainState).SchemaVersion()

if currentVersion < migrationScheme.BaseSchemaVersion {
Expand Down
15 changes: 0 additions & 15 deletions packages/vm/vmimpl/migrations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,21 +91,6 @@ func newMigrationsTest(t *testing.T, stateIndex uint32) *migrationsTestEnv {
return env
}

func TestMigrationsStateIndex0(t *testing.T) {
env := newMigrationsTest(t, 0)

require.EqualValues(t, 0, env.getSchemaVersion())

env.vmctx.withStateUpdate(func(chainState kv.KVStore) {
env.vmctx.runMigrations(chainState, &migrations.MigrationScheme{
BaseSchemaVersion: 0,
Migrations: []migrations.Migration{env.panic, env.panic, env.panic},
})
})

require.EqualValues(t, 3, env.getSchemaVersion())
}

func TestMigrationsStateIndex1(t *testing.T) {
env := newMigrationsTest(t, 1)

Expand Down

0 comments on commit 01f56f2

Please sign in to comment.