diff --git a/packages/vm/core/evm/emulator/statedb.go b/packages/vm/core/evm/emulator/statedb.go index 5505a93785..373a7b1785 100644 --- a/packages/vm/core/evm/emulator/statedb.go +++ b/packages/vm/core/evm/emulator/statedb.go @@ -67,10 +67,10 @@ func NewStateDB(ctx Context) *StateDB { // NewStateDBFromKVStore Creates a StateDB without any context. Handle with care. Functions requiring the context will crash. // It is currently only used for the ERC721 registration using a KVStore which doesn't justify a new class. -func NewStateDBFromKVStore(store kv.KVStore) *StateDB { +func NewStateDBFromKVStore(emulatorState kv.KVStore) *StateDB { return &StateDB{ ctx: nil, - kv: StateDBSubrealm(store), + kv: StateDBSubrealm(emulatorState), snapshots: make(map[int][]*types.Log), } } diff --git a/packages/vm/core/evm/evmimpl/external.go b/packages/vm/core/evm/evmimpl/external.go index c0481056f4..1b13e64fa7 100644 --- a/packages/vm/core/evm/evmimpl/external.go +++ b/packages/vm/core/evm/evmimpl/external.go @@ -17,14 +17,14 @@ func Nonce(evmPartition kv.KVStoreReader, addr common.Address) uint64 { return emulator.GetNonce(stateDBStore, addr) } -func RegisterERC721NFTCollectionByNFTId(store kv.KVStore, nft *isc.NFT) { +func RegisterERC721NFTCollectionByNFTId(evmState kv.KVStore, nft *isc.NFT) { metadata, err := isc.IRC27NFTMetadataFromBytes(nft.Metadata) if err != nil { panic(errEVMCanNotDecodeERC27Metadata) } addr := iscmagic.ERC721NFTCollectionAddress(nft.ID) - state := emulator.NewStateDBFromKVStore(emulator.StateDBSubrealm(store)) + state := emulator.NewStateDBFromKVStore(evm.EmulatorStateSubrealm(evmState)) if state.Exist(addr) { panic(errEVMAccountAlreadyExists) @@ -38,5 +38,5 @@ func RegisterERC721NFTCollectionByNFTId(store kv.KVStore, nft *isc.NFT) { state.SetState(addr, k, v) } - addToPrivileged(store, addr) + addToPrivileged(evmState, addr) } diff --git a/packages/vm/core/evm/evmimpl/impl.go b/packages/vm/core/evm/evmimpl/impl.go index 14563ada38..6221afb1a0 100644 --- a/packages/vm/core/evm/evmimpl/impl.go +++ b/packages/vm/core/evm/evmimpl/impl.go @@ -5,10 +5,13 @@ package evmimpl import ( "encoding/hex" + "math/big" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" "github.com/samber/lo" iotago "github.com/iotaledger/iota.go/v3" @@ -465,10 +468,38 @@ func newL1Deposit(ctx isc.Sandbox) dict.Dict { }, ) - // create a fake receipt + logs := make([]*types.Log, 0) + for _, nt := range assets.NativeTokens { + if nt.Amount.Sign() == 0 { + continue + } + // emit a Transfer event from the ERC20NativeTokens / ERC20ExternalNativeTokens contract + erc20Address, ok := findERC20NativeTokenContractAddress(ctx, nt.ID) + if !ok { + continue + } + logs = append(logs, makeTransferEvent(erc20Address, addr, nt.Amount)) + } + for _, nftID := range assets.NFTs { + // if the NFT belongs to a collection, emit a Transfer event from the corresponding ERC721NFTCollection contract + if nft := ctx.GetNFTData(nftID); nft != nil { + if collectionNFTAddress, ok := nft.Issuer.(*iotago.NFTAddress); ok { + collectionID := collectionNFTAddress.NFTID() + erc721CollectionContractAddress := iscmagic.ERC721NFTCollectionAddress(collectionID) + stateDB := emulator.NewStateDB(newEmulatorContext(ctx)) + if stateDB.Exist(erc721CollectionContractAddress) { + logs = append(logs, makeTransferEvent(erc721CollectionContractAddress, addr, iscmagic.WrapNFTID(nftID).TokenID())) + continue + } + } + } + // otherwise, emit a Transfer event from the ERC721NFTs contract + logs = append(logs, makeTransferEvent(iscmagic.ERC721NFTsAddress, addr, iscmagic.WrapNFTID(nftID).TokenID())) + } + receipt := &types.Receipt{ Type: types.LegacyTxType, - Logs: make([]*types.Log, 0), + Logs: logs, Status: types.ReceiptStatusSuccessful, } receipt.Bloom = types.CreateBloom(types.Receipts{receipt}) @@ -481,3 +512,19 @@ func newL1Deposit(ctx isc.Sandbox) dict.Dict { return nil } + +var transferEventTopic = crypto.Keccak256Hash([]byte("Transfer(address,address,uint256)")) + +func makeTransferEvent(contractAddress, to common.Address, uint256Data *big.Int) *types.Log { + var addrTopic common.Hash + copy(addrTopic[len(addrTopic)-len(to):], to[:]) + return &types.Log{ + Address: contractAddress, + Topics: []common.Hash{ + transferEventTopic, // event topic + {}, // indexed `from` address + addrTopic, // indexed `to` address + }, + Data: lo.Must((abi.Arguments{{Type: lo.Must(abi.NewType("uint256", "", nil))}}).Pack(uint256Data)), + } +} diff --git a/packages/vm/core/evm/evmimpl/iscmagic_state.go b/packages/vm/core/evm/evmimpl/iscmagic_state.go index f82b9881a9..7e5ad19738 100644 --- a/packages/vm/core/evm/evmimpl/iscmagic_state.go +++ b/packages/vm/core/evm/evmimpl/iscmagic_state.go @@ -16,6 +16,7 @@ import ( "github.com/iotaledger/wasp/packages/kv" "github.com/iotaledger/wasp/packages/vm/core/errors/coreerrors" "github.com/iotaledger/wasp/packages/vm/core/evm" + "github.com/iotaledger/wasp/packages/vm/core/evm/emulator" "github.com/iotaledger/wasp/packages/vm/core/evm/iscmagic" ) @@ -45,8 +46,8 @@ func isCallerPrivileged(ctx isc.SandboxBase, addr common.Address) bool { return state.Has(keyPrivileged(addr)) } -func addToPrivileged(s kv.KVStore, addr common.Address) { - state := evm.ISCMagicSubrealm(s) +func addToPrivileged(evmState kv.KVStore, addr common.Address) { + state := evm.ISCMagicSubrealm(evmState) state.Set(keyPrivileged(addr), []byte{1}) } @@ -137,3 +138,18 @@ func getERC20ExternalNativeTokensAddress(ctx isc.SandboxBase, nativeTokenID iota copy(ret[:], b) return ret, true } + +// findERC20NativeTokenContractAddress returns the address of an +// ERC20NativeTokens or ERC20ExternalNativeTokens contract. +func findERC20NativeTokenContractAddress(ctx isc.Sandbox, nativeTokenID iotago.NativeTokenID) (common.Address, bool) { + addr, ok := getERC20ExternalNativeTokensAddress(ctx, nativeTokenID) + if ok { + return addr, true + } + addr = iscmagic.ERC20NativeTokensAddress(nativeTokenID.FoundrySerialNumber()) + stateDB := emulator.NewStateDB(newEmulatorContext(ctx)) + if stateDB.Exist(addr) { + return addr, true + } + return common.Address{}, false +} diff --git a/packages/vm/core/evm/evmtest/evm_test.go b/packages/vm/core/evm/evmtest/evm_test.go index c4364bcdec..08812badce 100644 --- a/packages/vm/core/evm/evmtest/evm_test.go +++ b/packages/vm/core/evm/evmtest/evm_test.go @@ -918,6 +918,22 @@ func TestERC721NFTs(t *testing.T) { require.NoError(t, err) env.Chain.MustDepositNFT(nft, ethAgentID, env.Chain.OriginatorPrivateKey) + // there must be a Transfer event emitted from the ERC721NFTs contract + { + blockTxs := env.latestEVMTxs() + require.Len(t, blockTxs, 1) + tx := blockTxs[0] + receipt := env.evmChain.TransactionReceipt(tx.Hash()) + require.Len(t, receipt.Logs, 1) + checkTransferEvent( + t, + receipt.Logs[0], + iscmagic.ERC721NFTsAddress, + ethAddr, + iscmagic.WrapNFTID(nft.ID).TokenID(), + ) + } + { var n *big.Int erc721.callView("balanceOf", []any{ethAddr}, &n) @@ -1011,17 +1027,40 @@ func TestERC721NFTCollection(t *testing.T) { require.True(t, env.solo.HasL1NFT(collectionOwnerAddr, &nft.ID)) } - // deposit all nfts on L2 - nfts := func() []*isc.NFT { - var nfts []*isc.NFT + // deposit the collection NFT in the owner's L2 account + collectionNFT, _ := lo.Find(allNFTs, func(nft *isc.NFT) bool { return nft.ID == collection.ID }) + env.Chain.MustDepositNFT(collectionNFT, isc.NewAgentID(collectionOwnerAddr), collectionOwner) + + err = env.registerERC721NFTCollection(collectionOwner, collection.ID) + require.NoError(t, err) + + // should not allow to register again + err = env.registerERC721NFTCollection(collectionOwner, collection.ID) + require.ErrorContains(t, err, "already exists") + + // deposit the two nfts of the collection on ethAddr's L2 account + nfts := func() (nfts []*isc.NFT) { for _, nft := range allNFTs { if nft.ID == collection.ID { - // the collection NFT in the owner's account - env.Chain.MustDepositNFT(nft, isc.NewAgentID(collectionOwnerAddr), collectionOwner) - } else { - // others in ethAgentID's account - env.Chain.MustDepositNFT(nft, ethAgentID, collectionOwner) - nfts = append(nfts, nft) + continue + } + env.Chain.MustDepositNFT(nft, ethAgentID, collectionOwner) + nfts = append(nfts, nft) + + // there must be a Transfer event emitted from the ERC721NFTCollection contract + { + blockTxs := env.latestEVMTxs() + require.Len(t, blockTxs, 1) + tx := blockTxs[0] + receipt := env.evmChain.TransactionReceipt(tx.Hash()) + require.Len(t, receipt.Logs, 1) + checkTransferEvent( + t, + receipt.Logs[0], + iscmagic.ERC721NFTCollectionAddress(collection.ID), + ethAddr, + iscmagic.WrapNFTID(nft.ID).TokenID(), + ) } } return nfts @@ -1036,13 +1075,6 @@ func TestERC721NFTCollection(t *testing.T) { }) require.True(t, ok) - err = env.registerERC721NFTCollection(collectionOwner, collection.ID) - require.NoError(t, err) - - // should not allow to register again - err = env.registerERC721NFTCollection(collectionOwner, collection.ID) - require.ErrorContains(t, err, "already exists") - erc721 := env.ERC721NFTCollection(ethKey, collection.ID) { @@ -1339,6 +1371,24 @@ func TestERC20BaseTokens(t *testing.T) { } } +func checkTransferEvent( + t *testing.T, + log *types.Log, + contractAddress, to common.Address, + uint256Data *big.Int, +) { + require.Equal(t, contractAddress, log.Address) + + require.Len(t, log.Topics, 3) + require.Equal(t, crypto.Keccak256Hash([]byte("Transfer(address,address,uint256)")), log.Topics[0]) + var addrTopic common.Hash + copy(addrTopic[len(addrTopic)-len(to):], to[:]) + require.Equal(t, addrTopic, log.Topics[2]) + + data := lo.Must((abi.Arguments{{Type: lo.Must(abi.NewType("uint256", "", nil))}}).Unpack(log.Data))[0].(*big.Int) + require.Zero(t, uint256Data.Cmp(data)) +} + func TestERC20NativeTokens(t *testing.T) { env := InitEVM(t, false) @@ -1359,7 +1409,6 @@ func TestERC20NativeTokens(t *testing.T) { WithTokenSymbol(tokenTickerSymbol). WithTokenDecimals(tokenDecimals). CreateFoundry() - require.NoError(t, err) err = env.Chain.MintTokens(foundrySN, supply, foundryOwner) require.NoError(t, err) @@ -1376,6 +1425,22 @@ func TestERC20NativeTokens(t *testing.T) { }), ethAgentID, foundryOwner) require.NoError(t, err) + // there must be a Transfer event emitted from the ERC20NativeTokens contract + { + blockTxs := env.latestEVMTxs() + require.Len(t, blockTxs, 1) + tx := blockTxs[0] + receipt := env.evmChain.TransactionReceipt(tx.Hash()) + require.Len(t, receipt.Logs, 1) + checkTransferEvent( + t, + receipt.Logs[0], + iscmagic.ERC20NativeTokensAddress(foundrySN), + ethAddr, + supply, + ) + } + { sandbox := env.ISCMagicSandbox(ethKey) var addr common.Address @@ -2465,9 +2530,7 @@ func TestL1DepositEVM(t *testing.T) { require.NoError(t, err) // previous block must only have 1 tx, that corresponds to the deposit to ethAddr - block, err := env.Chain.EVM().BlockByNumber(big.NewInt(int64(env.getBlockNumber()))) - require.NoError(t, err) - blockTxs := block.Transactions() + blockTxs := env.latestEVMTxs() require.Len(t, blockTxs, 1) tx := blockTxs[0] require.True(t, tx.GasPrice().Cmp(util.Big0) == 1) @@ -2484,7 +2547,7 @@ func TestL1DepositEVM(t *testing.T) { // blockIndex blockIndex := rr.ReadUint32() - require.Equal(t, block.Number().Uint64(), uint64(blockIndex)) + require.Equal(t, env.evmChain.BlockNumber().Uint64(), uint64(blockIndex)) reqIndex := rr.ReadUint16() require.Zero(t, reqIndex) n, err := buf.Read([]byte{}) @@ -2513,9 +2576,7 @@ func TestL1DepositEVM(t *testing.T) { ) require.NoError(t, err) - block2, err := env.Chain.EVM().BlockByNumber(big.NewInt(int64(env.getBlockNumber()))) - require.NoError(t, err) - blockTxs2 := block2.Transactions() + blockTxs2 := env.latestEVMTxs() require.Len(t, blockTxs2, 1) tx2 := blockTxs2[0] require.NotEqual(t, tx.Hash(), tx2.Hash()) diff --git a/packages/vm/core/evm/evmtest/setup.go b/packages/vm/core/evm/evmtest/setup.go index 4c05d3d341..72c09f8214 100644 --- a/packages/vm/core/evm/evmtest/setup.go +++ b/packages/vm/core/evm/evmtest/setup.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" "github.com/stretchr/testify/require" iotago "github.com/iotaledger/iota.go/v3" @@ -178,14 +179,14 @@ func (e *SoloChainEnv) ERC20BaseTokens(defaultSender *ecdsa.PrivateKey) *IscCont } func (e *SoloChainEnv) ERC20NativeTokens(defaultSender *ecdsa.PrivateKey, foundrySN uint32) *IscContractInstance { - erc20BaseABI, err := abi.JSON(strings.NewReader(iscmagic.ERC20NativeTokensABI)) + ntABI, err := abi.JSON(strings.NewReader(iscmagic.ERC20NativeTokensABI)) require.NoError(e.t, err) return &IscContractInstance{ EVMContractInstance: &EVMContractInstance{ chain: e, defaultSender: defaultSender, address: iscmagic.ERC20NativeTokensAddress(foundrySN), - abi: erc20BaseABI, + abi: ntABI, }, } } @@ -291,3 +292,9 @@ func (e *SoloChainEnv) registerERC721NFTCollection(collectionOwner *cryptolib.Ke }).WithMaxAffordableGasBudget(), collectionOwner) return err } + +func (e *SoloChainEnv) latestEVMTxs() types.Transactions { + block, err := e.Chain.EVM().BlockByNumber(nil) + require.NoError(e.t, err) + return block.Transactions() +}