From 30dd880fc8d72638e6bd3c872366a5587ccba138 Mon Sep 17 00:00:00 2001 From: Diego Essaya Date: Wed, 22 May 2024 12:07:58 -0300 Subject: [PATCH 1/2] evm: emit Transfer events on L1 deposits --- packages/vm/core/evm/emulator/statedb.go | 4 +- packages/vm/core/evm/evmimpl/external.go | 6 +- packages/vm/core/evm/evmimpl/impl.go | 51 +++++++- .../vm/core/evm/evmimpl/iscmagic_state.go | 20 ++- packages/vm/core/evm/evmtest/evm_test.go | 117 ++++++++++++++---- packages/vm/core/evm/evmtest/setup.go | 11 +- 6 files changed, 174 insertions(+), 35 deletions(-) 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..1984736733 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 { + // emit a Transfer event from the ERC721NFTs contract + logs = append(logs, makeTransferEvent(iscmagic.ERC721NFTsAddress, addr, iscmagic.WrapNFTID(nftID).TokenID())) + + // 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())) + } + } + } + } + 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..98e0ed82f1 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,48 @@ 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 two Transfer events: from the ERC721NFTs and + // ERC721NFTCollection contracts + { + blockTxs := env.latestEVMTxs() + require.Len(t, blockTxs, 1) + tx := blockTxs[0] + receipt := env.evmChain.TransactionReceipt(tx.Hash()) + require.Len(t, receipt.Logs, 2) + checkTransferEvent( + t, + receipt.Logs[0], + iscmagic.ERC721NFTsAddress, + ethAddr, + iscmagic.WrapNFTID(nft.ID).TokenID(), + ) + checkTransferEvent( + t, + receipt.Logs[1], + iscmagic.ERC721NFTCollectionAddress(collection.ID), + ethAddr, + iscmagic.WrapNFTID(nft.ID).TokenID(), + ) } } return nfts @@ -1036,13 +1083,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 +1379,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 +1417,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 +1433,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 +2538,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 +2555,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 +2584,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() +} From a606d8063369af1d5502a896fe24b2c5f321460a Mon Sep 17 00:00:00 2001 From: Diego Essaya Date: Tue, 28 May 2024 11:33:50 -0300 Subject: [PATCH 2/2] evm: emit a single event if nft belongs to collection --- packages/vm/core/evm/evmimpl/impl.go | 6 +++--- packages/vm/core/evm/evmtest/evm_test.go | 12 ++---------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/packages/vm/core/evm/evmimpl/impl.go b/packages/vm/core/evm/evmimpl/impl.go index 1984736733..6221afb1a0 100644 --- a/packages/vm/core/evm/evmimpl/impl.go +++ b/packages/vm/core/evm/evmimpl/impl.go @@ -481,9 +481,6 @@ func newL1Deposit(ctx isc.Sandbox) dict.Dict { logs = append(logs, makeTransferEvent(erc20Address, addr, nt.Amount)) } for _, nftID := range assets.NFTs { - // emit a Transfer event from the ERC721NFTs contract - logs = append(logs, makeTransferEvent(iscmagic.ERC721NFTsAddress, addr, iscmagic.WrapNFTID(nftID).TokenID())) - // 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 { @@ -492,9 +489,12 @@ func newL1Deposit(ctx isc.Sandbox) dict.Dict { 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{ diff --git a/packages/vm/core/evm/evmtest/evm_test.go b/packages/vm/core/evm/evmtest/evm_test.go index 98e0ed82f1..08812badce 100644 --- a/packages/vm/core/evm/evmtest/evm_test.go +++ b/packages/vm/core/evm/evmtest/evm_test.go @@ -1047,24 +1047,16 @@ func TestERC721NFTCollection(t *testing.T) { env.Chain.MustDepositNFT(nft, ethAgentID, collectionOwner) nfts = append(nfts, nft) - // there must be two Transfer events: from the ERC721NFTs and - // ERC721NFTCollection contracts + // 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, 2) + require.Len(t, receipt.Logs, 1) checkTransferEvent( t, receipt.Logs[0], - iscmagic.ERC721NFTsAddress, - ethAddr, - iscmagic.WrapNFTID(nft.ID).TokenID(), - ) - checkTransferEvent( - t, - receipt.Logs[1], iscmagic.ERC721NFTCollectionAddress(collection.ID), ethAddr, iscmagic.WrapNFTID(nft.ID).TokenID(),