diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f7cb192f..2838cfd24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,14 +51,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Remove unnecessary argument in the `VerifyFee` function, which returns the token payment required based on the effective fee from the tx data. Improve documentation. -- [#2127](https://github.com/NibiruChain/nibiru/pull/2127) - fix(vesting): disabled built in auth/vesting module functionality - [#2125](https://github.com/NibiruChain/nibiru/pull/2125) - feat(evm-precompile):Emit EVM events created to reflect the ABCI events that occur outside the EVM to make sure that block explorers and indexers can find indexed ABCI event information. +- [#2127](https://github.com/NibiruChain/nibiru/pull/2127) - fix(vesting): disabled built in auth/vesting module functionality - [#2129](https://github.com/NibiruChain/nibiru/pull/2129) - fix(evm): issue with infinite recursion in erc20 funtoken contracts +- [#2130](https://github.com/NibiruChain/nibiru/pull/2130) - fix(evm): proper nonce management in statedb - [#2132](https://github.com/NibiruChain/nibiru/pull/2132) - fix(evm): proper tx gas refund - [#2134](https://github.com/NibiruChain/nibiru/pull/2134) - fix(evm): query of NIBI should use bank state, not the StateDB +- [#2139](https://github.com/NibiruChain/nibiru/pull/2139) - fix(evm): erc20 born funtoken: properly burn bank coins after converting coin back to erc20 - [#2140](https://github.com/NibiruChain/nibiru/pull/2140) - fix(bank): bank keeper extension now charges gas for the bank operations - [#2141](https://github.com/NibiruChain/nibiru/pull/2141) - refactor: simplify account retrieval operation in `nibid q evm account`. - [#2142](https://github.com/NibiruChain/nibiru/pull/2142) - fix(bank): add additional missing methods to the NibiruBankKeeper +- [#2144](https://github.com/NibiruChain/nibiru/pull/2144) - feat(token-registry): Implement strongly typed Nibiru Token Registry and generation command +- [#2145](https://github.com/NibiruChain/nibiru/pull/2145) - chore(token-registry): add xNIBI Astrovault LST to registry +- [#2147](https://github.com/NibiruChain/nibiru/pull/2147) - fix(simapp): manually add x/vesting Cosmos-SDK module types to the codec in simulation tests since they are expected by default #### Nibiru EVM | Before Audit 2 - 2024-12-06 diff --git a/eth/rpc/backend/backend_suite_test.go b/eth/rpc/backend/backend_suite_test.go index 8626527db..4c2116a1c 100644 --- a/eth/rpc/backend/backend_suite_test.go +++ b/eth/rpc/backend/backend_suite_test.go @@ -5,9 +5,11 @@ import ( "crypto/ecdsa" "fmt" "math/big" + "sync" "testing" "time" + "github.com/cosmos/cosmos-sdk/client/flags" sdk "github.com/cosmos/cosmos-sdk/types" gethcommon "github.com/ethereum/go-ethereum/common" gethcore "github.com/ethereum/go-ethereum/core/types" @@ -32,6 +34,9 @@ import ( "github.com/NibiruChain/nibiru/v2/x/common/testutil/testnetwork" ) +// testMutex is used to synchronize the tests which are broadcasting transactions concurrently +var testMutex sync.Mutex + var ( recipient = evmtest.NewEthPrivAcc().EthAddr amountToSend = evm.NativeToWei(big.NewInt(1)) @@ -115,13 +120,12 @@ func (s *BackendSuite) SendNibiViaEthTransfer( amount *big.Int, waitForNextBlock bool, ) gethcommon.Hash { - nonce, err := s.backend.GetTransactionCount(s.fundedAccEthAddr, rpc.EthPendingBlockNumber) - s.Require().NoError(err) + nonce := s.getCurrentNonce(s.fundedAccEthAddr) return SendTransaction( s, &gethcore.LegacyTx{ To: &to, - Nonce: uint64(*nonce), + Nonce: uint64(nonce), Value: amount, Gas: params.TxGas, GasPrice: big.NewInt(1), @@ -135,20 +139,20 @@ func (s *BackendSuite) DeployTestContract(waitForNextBlock bool) (gethcommon.Has packedArgs, err := embeds.SmartContract_TestERC20.ABI.Pack("") s.Require().NoError(err) bytecodeForCall := append(embeds.SmartContract_TestERC20.Bytecode, packedArgs...) - nonce, err := s.backend.GetTransactionCount(s.fundedAccEthAddr, rpc.EthPendingBlockNumber) + nonce := s.getCurrentNonce(s.fundedAccEthAddr) s.Require().NoError(err) txHash := SendTransaction( s, &gethcore.LegacyTx{ - Nonce: uint64(*nonce), + Nonce: uint64(nonce), Data: bytecodeForCall, Gas: 1500_000, GasPrice: big.NewInt(1), }, waitForNextBlock, ) - contractAddr := crypto.CreateAddress(s.fundedAccEthAddr, (uint64)(*nonce)) + contractAddr := crypto.CreateAddress(s.fundedAccEthAddr, nonce) return txHash, contractAddr } @@ -198,3 +202,69 @@ func (s *BackendSuite) getUnibiBalance(address gethcommon.Address) *big.Int { s.Require().NoError(err) return evm.WeiToNative(balance.ToInt()) } + +// getCurrentNonce returns the current nonce of the funded account +func (s *BackendSuite) getCurrentNonce(address gethcommon.Address) uint64 { + nonce, err := s.backend.GetTransactionCount(s.fundedAccEthAddr, rpc.EthPendingBlockNumber) + s.Require().NoError(err) + + return uint64(*nonce) +} + +// broadcastSDKTx broadcasts the given SDK transaction and returns the response +func (s *BackendSuite) broadcastSDKTx(sdkTx sdk.Tx) *sdk.TxResponse { + txBytes, err := s.backend.ClientCtx().TxConfig.TxEncoder()(sdkTx) + s.Require().NoError(err) + + syncCtx := s.backend.ClientCtx().WithBroadcastMode(flags.BroadcastSync) + rsp, err := syncCtx.BroadcastTx(txBytes) + s.Require().NoError(err) + return rsp +} + +// buildContractCreationTx builds a contract creation transaction +func (s *BackendSuite) buildContractCreationTx(nonce uint64) gethcore.Transaction { + packedArgs, err := embeds.SmartContract_TestERC20.ABI.Pack("") + s.Require().NoError(err) + bytecodeForCall := append(embeds.SmartContract_TestERC20.Bytecode, packedArgs...) + + creationTx := &gethcore.LegacyTx{ + Nonce: nonce, + Data: bytecodeForCall, + Gas: 1_500_000, + GasPrice: big.NewInt(1), + } + + signer := gethcore.LatestSignerForChainID(s.ethChainID) + signedTx, err := gethcore.SignNewTx(s.fundedAccPrivateKey, signer, creationTx) + s.Require().NoError(err) + + return *signedTx +} + +// buildContractCallTx builds a contract call transaction +func (s *BackendSuite) buildContractCallTx(nonce uint64, contractAddr gethcommon.Address) gethcore.Transaction { + //recipient := crypto.CreateAddress(s.fundedAccEthAddr, 29381) + transferAmount := big.NewInt(100) + + packedArgs, err := embeds.SmartContract_TestERC20.ABI.Pack( + "transfer", + recipient, + transferAmount, + ) + s.Require().NoError(err) + + transferTx := &gethcore.LegacyTx{ + Nonce: nonce, + Data: packedArgs, + Gas: 100_000, + GasPrice: big.NewInt(1), + To: &contractAddr, + } + + signer := gethcore.LatestSignerForChainID(s.ethChainID) + signedTx, err := gethcore.SignNewTx(s.fundedAccPrivateKey, signer, transferTx) + s.Require().NoError(err) + + return *signedTx +} diff --git a/eth/rpc/backend/gas_used_test.go b/eth/rpc/backend/gas_used_test.go index f62307e1a..2a655f771 100644 --- a/eth/rpc/backend/gas_used_test.go +++ b/eth/rpc/backend/gas_used_test.go @@ -19,6 +19,10 @@ import ( // Test creates 2 eth transfer txs that are supposed to be included in the same block. // It checks that gas used is the same for both txs and the total block gas is greater than the sum of 2 gas used. func (s *BackendSuite) TestGasUsedTransfers() { + // Test is broadcasting txs. Lock to avoid nonce conflicts. + testMutex.Lock() + defer testMutex.Unlock() + // Start with new block s.Require().NoError(s.network.WaitForNextBlock()) balanceBefore := s.getUnibiBalance(s.fundedAccEthAddr) @@ -64,6 +68,10 @@ func (s *BackendSuite) TestGasUsedTransfers() { // It also checks that txs are included in the same block and block gas is greater or equals // to the total gas used by txs. func (s *BackendSuite) TestGasUsedFunTokens() { + // Test is broadcasting txs. Lock to avoid nonce conflicts. + testMutex.Lock() + defer testMutex.Unlock() + // Create funtoken from erc20 erc20Addr, err := eth.NewEIP55AddrFromStr(testContractAddress.String()) s.Require().NoError(err) diff --git a/eth/rpc/backend/nonce_test.go b/eth/rpc/backend/nonce_test.go new file mode 100644 index 000000000..cd35c9088 --- /dev/null +++ b/eth/rpc/backend/nonce_test.go @@ -0,0 +1,79 @@ +package backend_test + +import ( + sdkmath "cosmossdk.io/math" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + gethcore "github.com/ethereum/go-ethereum/core/types" + + "github.com/NibiruChain/nibiru/v2/x/evm" +) + +// TestNonceIncrementWithMultipleMsgsTx tests that the nonce is incremented correctly +// when multiple messages are included in a single transaction. +func (s *BackendSuite) TestNonceIncrementWithMultipleMsgsTx() { + // Test is broadcasting txs. Lock to avoid nonce conflicts. + testMutex.Lock() + defer testMutex.Unlock() + + nonce := s.getCurrentNonce(s.fundedAccEthAddr) + + // Create series of 3 tx messages. Expecting nonce to be incremented by 3 + creationTx := s.buildContractCreationTx(nonce) + firstTransferTx := s.buildContractCallTx(nonce+1, testContractAddress) + secondTransferTx := s.buildContractCallTx(nonce+2, testContractAddress) + + // Create and broadcast SDK transaction + sdkTx := s.buildSDKTxWithEVMMessages( + creationTx, + firstTransferTx, + secondTransferTx, + ) + + // Broadcast transaction + rsp := s.broadcastSDKTx(sdkTx) + s.Assert().NotEqual(rsp.Code, 0) + s.Require().NoError(s.network.WaitForNextBlock()) + + // Expected nonce should be incremented by 3 + currentNonce := s.getCurrentNonce(s.fundedAccEthAddr) + s.Assert().Equal(nonce+3, currentNonce) + + // Assert all transactions included in block + for _, tx := range []gethcore.Transaction{creationTx, firstTransferTx, secondTransferTx} { + blockNum, blockHash, _ := WaitForReceipt(s, tx.Hash()) + s.Require().NotNil(blockNum) + s.Require().NotNil(blockHash) + } +} + +// buildSDKTxWithEVMMessages creates an SDK transaction with EVM messages +func (s *BackendSuite) buildSDKTxWithEVMMessages(txs ...gethcore.Transaction) sdk.Tx { + msgs := make([]sdk.Msg, len(txs)) + for i, tx := range txs { + msg := &evm.MsgEthereumTx{} + err := msg.FromEthereumTx(&tx) + s.Require().NoError(err) + msgs[i] = msg + } + + option, err := codectypes.NewAnyWithValue(&evm.ExtensionOptionsEthereumTx{}) + s.Require().NoError(err) + + txBuilder, _ := s.backend.ClientCtx().TxConfig.NewTxBuilder().(authtx.ExtensionOptionsTxBuilder) + txBuilder.SetExtensionOptions(option) + err = txBuilder.SetMsgs(msgs...) + s.Require().NoError(err) + + // Set fees for all messages + totalGas := uint64(0) + for _, tx := range txs { + totalGas += tx.Gas() + } + fees := sdk.NewCoins(sdk.NewCoin("unibi", sdkmath.NewIntFromUint64(totalGas))) + txBuilder.SetFeeAmount(fees) + txBuilder.SetGasLimit(totalGas) + + return txBuilder.GetTx() +} diff --git a/justfile b/justfile index 77b6e1aaf..175e1c5e9 100644 --- a/justfile +++ b/justfile @@ -41,6 +41,10 @@ gen-embeds: alias gen-proto := proto-gen +# Generate the Nibiru Token Registry files +gen-token-registry: + go run token-registry/main/main.go + # Generate protobuf-based types in Rust gen-proto-rs: bash proto/buf.gen.rs.sh diff --git a/simapp/sim_test.go b/simapp/sim_test.go index 3163428bd..ba58da5b8 100644 --- a/simapp/sim_test.go +++ b/simapp/sim_test.go @@ -21,6 +21,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + vesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" @@ -36,6 +37,7 @@ import ( "github.com/stretchr/testify/require" "github.com/NibiruChain/nibiru/v2/app" + "github.com/NibiruChain/nibiru/v2/app/codec" devgastypes "github.com/NibiruChain/nibiru/v2/x/devgas/v1/types" epochstypes "github.com/NibiruChain/nibiru/v2/x/epochs/types" inflationtypes "github.com/NibiruChain/nibiru/v2/x/inflation/types" @@ -59,6 +61,17 @@ type StoreKeysPrefixes struct { Prefixes [][]byte } +// makeEncodingConfig, similar to [app.MakeEncodingConfig], creates an +// EncodingConfig for an amino based test configuration. However, this function +// registers interfaces and types that are expected by default in the Cosmos-SDK +// even if they are disabled on Nibiru. This is the case for x/vesting Cosmos-SDK +// module. +func makeEncodingConfig() codec.EncodingConfig { + encCfg := app.MakeEncodingConfig() + vesting.RegisterInterfaces(encCfg.InterfaceRegistry) + return encCfg +} + func TestFullAppSimulation(t *testing.T) { config := simcli.NewConfigFromFlags() config.ChainID = SimAppChainID @@ -78,7 +91,7 @@ func TestFullAppSimulation(t *testing.T) { appOptions[flags.FlagHome] = app.DefaultNodeHome appOptions[server.FlagInvCheckPeriod] = simcli.FlagPeriodValue - encoding := app.MakeEncodingConfig() + encoding := makeEncodingConfig() app := app.NewNibiruApp(logger, db, nil, true, encoding, appOptions, baseapp.SetChainID(SimAppChainID)) require.Equal(t, "Nibiru", app.Name()) appCodec := app.AppCodec() @@ -132,7 +145,7 @@ func TestAppStateDeterminism(t *testing.T) { for j := 0; j < numTimesToRunPerSeed; j++ { db := dbm.NewMemDB() logger := log.NewNopLogger() - encoding := app.MakeEncodingConfig() + encoding := makeEncodingConfig() app := app.NewNibiruApp(logger, db, nil, true, encoding, appOptions, baseapp.SetChainID(SimAppChainID)) appCodec := app.AppCodec() @@ -191,7 +204,7 @@ func TestAppImportExport(t *testing.T) { appOptions[flags.FlagHome] = app.DefaultNodeHome appOptions[server.FlagInvCheckPeriod] = simcli.FlagPeriodValue - encoding := app.MakeEncodingConfig() + encoding := makeEncodingConfig() oldApp := app.NewNibiruApp(logger, db, nil, true, encoding, appOptions, baseapp.SetChainID(SimAppChainID)) require.Equal(t, "Nibiru", oldApp.Name()) appCodec := oldApp.AppCodec() @@ -315,7 +328,7 @@ func TestAppSimulationAfterImport(t *testing.T) { appOptions[flags.FlagHome] = app.DefaultNodeHome appOptions[server.FlagInvCheckPeriod] = simcli.FlagPeriodValue - encoding := app.MakeEncodingConfig() + encoding := makeEncodingConfig() oldApp := app.NewNibiruApp(logger, db, nil, true, encoding, appOptions, baseapp.SetChainID(SimAppChainID)) require.Equal(t, "Nibiru", oldApp.Name()) appCodec := oldApp.AppCodec() diff --git a/token-registry/README.md b/token-registry/README.md new file mode 100644 index 000000000..68f97316c --- /dev/null +++ b/token-registry/README.md @@ -0,0 +1,5 @@ +# Nibiru/token-registry + +This directory implements the Nibiru Token Registry by providing a means to +register offchain digital token metadata to onchain identifiers for use with +applications like wallets. diff --git a/token-registry/assetlist.go b/token-registry/assetlist.go new file mode 100644 index 000000000..f13086fe4 --- /dev/null +++ b/token-registry/assetlist.go @@ -0,0 +1,227 @@ +package tokenregistry + +func NibiruAssetList() AssetList { + var tokens = TOKENS() + for idx, token := range tokens { + tokens[idx] = token.GitHubify() + } + + return AssetList{ + Schema: "../assetlist.schema.json", + ChainName: "nibiru", + Assets: tokens, + } +} + +func TOKENS() []Token { + return []Token{ + { + Name: "Nibiru", + Description: "The native token of Nibiru blockchain", + ExtendedDescription: some("Nibiru is a smart contract ecosystem with a high-performance, EVM-equivalent execution layer. Nibiru is engineered to meet the growing demand for versatile, scalable, and easy-to-use Web3 applications."), + Socials: &SocialLinks{ + Website: some("https://nibiru.fi"), + Twitter: some("https://twitter.com/nibiruchain"), + }, + DenomUnits: []DenomUnit{ + {Denom: "unibi", Exponent: 0}, + {Denom: "nibi", Exponent: 6}, + {Denom: "attonibi", Exponent: 18}, + }, + Base: "unibi", + Display: "nibi", + Symbol: "NIBI", + LogoURIs: &LogoURIs{ + Png: some("./img/0000_nibiru.png"), + Svg: some("./img/0000_nibiru.svg"), + }, + CoingeckoID: some("nibiru"), + Images: []AssetImage{ + { + Png: some("./img/0000_nibiru.png"), + Svg: some("./img/0000_nibiru.svg"), + Theme: &ImageTheme{ + PrimaryColorHex: some("#14c0ce"), + }, + }, + }, + TypeAsset: TypeAsset_SDKCoin, + }, + { + Name: "Liquid Staked Nibiru (Eris)", + Description: "Liquid Staked Nibiru (Eris)", + ExtendedDescription: some("Liquid Staked Nibiru, powered by Eris Protocol's amplifier contracts. Nibiru is a smart contract ecosystem with a high-performance, EVM-equivalent execution layer. Nibiru is engineered to meet the growing demand for versatile, scalable, and easy-to-use Web3 applications."), + Socials: &SocialLinks{ + Website: some("https://nibiru.fi/docs/learn/liquid-stake/"), + Twitter: some("https://x.com/eris_protocol"), + }, + DenomUnits: []DenomUnit{ + {Denom: "tf/nibi1udqqx30cw8nwjxtl4l28ym9hhrp933zlq8dqxfjzcdhvl8y24zcqpzmh8m/ampNIBI", Exponent: 0}, + {Denom: "stNIBI", Exponent: 6}, + }, + Base: "tf/nibi1udqqx30cw8nwjxtl4l28ym9hhrp933zlq8dqxfjzcdhvl8y24zcqpzmh8m/ampNIBI", + Display: "stNIBI", + Symbol: "stNIBI", + LogoURIs: &LogoURIs{ + Png: some("./img/0001_stnibi-500x500.png"), + Svg: some("./img/0001_stnibi-500x500.svg"), + }, + Images: []AssetImage{ + { + Png: some("./img/0001_stnibi-500x500.png"), + Svg: some("./img/0001_stnibi-500x500.svg"), + Theme: &ImageTheme{ + PrimaryColorHex: some("#14c0ce"), + }, + }, + }, + Traces: []Trace{ + { + Type: "liquid-stake", + Counterparty: Counterparty{ + ChainName: "nibiru", + BaseDenom: "unibi", + }, + Provider: some("Eris Protocol"), + }, + }, + TypeAsset: TypeAsset_SDKCoin, + }, + { + Name: "Noble USDC", + Description: "Noble USDC on Nibiru", + DenomUnits: []DenomUnit{ + {Denom: "ibc/F082B65C88E4B6D5EF1DB243CDA1D331D002759E938A0F5CD3FFDC5D53B3E349", Exponent: 0}, + {Denom: "usdc", Exponent: 6}, + }, + Base: "ibc/F082B65C88E4B6D5EF1DB243CDA1D331D002759E938A0F5CD3FFDC5D53B3E349", + Display: "usdc", + Symbol: "USDC", + Traces: []Trace{ + { + Type: TraceType_IBC, + Counterparty: Counterparty{ + ChainName: "noble", + BaseDenom: "uusdc", + ChannelID: some("channel-67"), + }, + Chain: &TraceChainInfo{ + ChannelID: "channel-2", + Path: "transfer/channel-2/uusdc", + }, + }, + }, + Images: []AssetImage{ + { + ImageSync: &ImageSync{ + ChainName: "noble", + BaseDenom: "uusdc", + }, + Png: some("./img/0002_usdc.png"), + Svg: some("./img/0002_usdc.svg"), + Theme: &ImageTheme{ + Circle: some(true), + PrimaryColorHex: some("#2775CA"), + }, + }, + }, + LogoURIs: &LogoURIs{ + Png: some("./img/0002_usdc.png"), + Svg: some("./img/0002_usdc.svg"), + }, + TypeAsset: TypeAsset_ICS20, + }, + + { + Name: "Astrovault token", + Description: "AXV", + ExtendedDescription: some("AXV is the Astrovault token."), + Socials: &SocialLinks{ + Website: some("https://astrovault.io/"), + Twitter: some("https://x.com/axvdex"), + }, + DenomUnits: []DenomUnit{ + {Denom: "tf/nibi1vetfuua65frvf6f458xgtjerf0ra7wwjykrdpuyn0jur5x07awxsfka0ga/axv", Exponent: 0}, + {Denom: "AXV", Exponent: 6}, + }, + Base: "tf/nibi1vetfuua65frvf6f458xgtjerf0ra7wwjykrdpuyn0jur5x07awxsfka0ga/axv", + Display: "AXV", + Symbol: "AXV", + LogoURIs: &LogoURIs{ + Png: some("./img/0003_astrovault-axv.png"), + Svg: some("./img/0003_astrovault-axv.svg"), + }, + Images: []AssetImage{ + { + ImageSync: &ImageSync{ + ChainName: "neutron", + BaseDenom: "cw20:neutron10dxyft3nv4vpxh5vrpn0xw8geej8dw3g39g7nqp8mrm307ypssksau29af", + }, + Png: some("./img/0003_astrovault-axv.png"), + Svg: some("./img/0003_astrovault-axv.svg"), + }, + }, + TypeAsset: TypeAsset_SDKCoin, + }, + + { + Name: "Astrovault Nibiru LST (xNIBI)", + Description: "Astrovault Nibiru LST (xNIBI)", + TypeAsset: TypeAsset_CW20, + ExtendedDescription: some("xNIBI is a liquid staking derivative for NIBI created by Astrovault."), + Socials: &SocialLinks{ + Website: some("https://astrovault.io/"), + Twitter: some("https://x.com/axvdex"), + }, + DenomUnits: []DenomUnit{ + {Denom: "cw20:nibi1cehpv50vl90g9qkwwny8mw7txw79zs6f7wsfe8ey7dgp238gpy4qhdqjhm", Exponent: 0}, + {Denom: "xNIBI", Exponent: 6}, + }, + Base: "cw20:nibi1cehpv50vl90g9qkwwny8mw7txw79zs6f7wsfe8ey7dgp238gpy4qhdqjhm", + Display: "xNIBI", + Symbol: "xNIBI", + LogoURIs: &LogoURIs{ + Svg: some("./img/0004_astrovault-xnibi.svg"), + }, + Images: []AssetImage{ + { + Svg: some("./img/0004_astrovault-xnibi.svg"), + }, + }, + }, + + { + Description: "uoprek", + DenomUnits: []DenomUnit{ + {Denom: "tf/nibi149m52kn7nvsg5nftvv4fh85scsavpdfxp5nr7zasz97dum89dp5qkyhy0t/uoprek", Exponent: 0}, + }, + Base: "tf/nibi149m52kn7nvsg5nftvv4fh85scsavpdfxp5nr7zasz97dum89dp5qkyhy0t/uoprek", + Name: "uoprek", + Display: "tf/nibi149m52kn7nvsg5nftvv4fh85scsavpdfxp5nr7zasz97dum89dp5qkyhy0t/uoprek", + Symbol: "UOPREK", + TypeAsset: TypeAsset_SDKCoin, + }, + { + Description: "utestate", + DenomUnits: []DenomUnit{ + {Denom: "tf/nibi1lp28kx3gz0prsztl024z730ufkg3alahaq3e7a6gae22nk0dqdvsyrrgqw/utestate", Exponent: 0}, + }, + Base: "tf/nibi1lp28kx3gz0prsztl024z730ufkg3alahaq3e7a6gae22nk0dqdvsyrrgqw/utestate", + Name: "utestate", + Display: "tf/nibi1lp28kx3gz0prsztl024z730ufkg3alahaq3e7a6gae22nk0dqdvsyrrgqw/utestate", + Symbol: "UTESTATE", + TypeAsset: TypeAsset_SDKCoin, + }, + { + Description: "npp", + DenomUnits: []DenomUnit{ + {Denom: "tf/nibi1xpp7yn0tce62ffattws3gpd6v0tah0mlevef3ej3r4pnfvsehcgqk3jvxq/NPP", Exponent: 0}, + }, + Base: "tf/nibi1xpp7yn0tce62ffattws3gpd6v0tah0mlevef3ej3r4pnfvsehcgqk3jvxq/NPP", + Name: "npp", + Display: "tf/nibi1xpp7yn0tce62ffattws3gpd6v0tah0mlevef3ej3r4pnfvsehcgqk3jvxq/NPP", + Symbol: "NPP", + TypeAsset: TypeAsset_SDKCoin, + }, + } +} diff --git a/token-registry/assetlist_test.go b/token-registry/assetlist_test.go new file mode 100644 index 000000000..06fcfb1c1 --- /dev/null +++ b/token-registry/assetlist_test.go @@ -0,0 +1,182 @@ +package tokenregistry_test + +import ( + "os" + "strings" + "testing" + + "github.com/stretchr/testify/suite" + + tokenregistry "github.com/NibiruChain/nibiru/v2/token-registry" +) + +type Suite struct { + suite.Suite +} + +func TestSuite(t *testing.T) { + suite.Run(t, new(Suite)) +} + +func (s *Suite) TestImagesPresent() { + assetList := tokenregistry.NibiruAssetList() + for _, token := range assetList.Assets { + s.Run(token.Name, func() { + token = token.GitHubify() + + // Make sure all local images in Token.LogoURIs exist + if token.LogoURIs != nil { + png, svg := token.LogoURIs.Png, token.LogoURIs.Svg + if png != nil && strings.Contains(*png, "token-registry/img/") { + localImgPath := "./img/" + strings.Split(*png, "/img/")[1] + _, err := os.Stat(localImgPath) + s.NoError(err) + } + if svg != nil && strings.Contains(*svg, "token-registry/img/") { + localImgPath := "./img/" + strings.Split(*svg, "/img/")[1] + _, err := os.Stat(localImgPath) + s.NoError(err) + } + } + + // Make sure all local images in Token.Images exist + for _, img := range token.Images { + png, svg := img.Png, img.Svg + if png != nil && strings.Contains(*png, "token-registry/img/") { + localImgPath := "./img/" + strings.Split(*png, "/img/")[1] + _, err := os.Stat(localImgPath) + s.NoError(err) + } + if svg != nil && strings.Contains(*svg, "token-registry/img/") { + localImgPath := "./img/" + strings.Split(*svg, "/img/")[1] + _, err := os.Stat(localImgPath) + s.NoError(err) + } + } + }) + } +} + +func some(s string) *string { + return &s +} + +func (s *Suite) TestLogoURIsGitHubify() { + tests := []struct { + name string + input tokenregistry.LogoURIs + expected tokenregistry.LogoURIs + }{ + { + name: "Png and Svg are local paths", + input: tokenregistry.LogoURIs{ + Png: some("./img/logo.png"), + Svg: some("./img/logo.svg"), + }, + expected: tokenregistry.LogoURIs{ + Png: some("https://raw.githubusercontent.com/NibiruChain/nibiru/main/token-registry/img/logo.png"), + Svg: some("https://raw.githubusercontent.com/NibiruChain/nibiru/main/token-registry/img/logo.svg"), + }, + }, + { + name: "Png is local, Svg is external", + input: tokenregistry.LogoURIs{ + Png: some("./img/logo.png"), + Svg: some("https://example.com/logo.svg"), + }, + expected: tokenregistry.LogoURIs{ + Png: some("https://raw.githubusercontent.com/NibiruChain/nibiru/main/token-registry/img/logo.png"), + Svg: some("https://example.com/logo.svg"), + }, + }, + { + name: "Both Png and Svg are external URLs", + input: tokenregistry.LogoURIs{ + Png: some("https://example.com/logo.png"), + Svg: some("https://example.com/logo.svg"), + }, + expected: tokenregistry.LogoURIs{ + Png: some("https://example.com/logo.png"), + Svg: some("https://example.com/logo.svg"), + }, + }, + { + name: "Both Png and Svg are nil", + input: tokenregistry.LogoURIs{ + Png: nil, + Svg: nil, + }, + expected: tokenregistry.LogoURIs{ + Png: nil, + Svg: nil, + }, + }, + } + + for _, tt := range tests { + s.Run(tt.name, func() { + result := tt.input.GitHubify() + s.Equal(tt.expected, *result) + }) + } +} + +func (s *Suite) TestAssetImageGitHubify() { + tests := []struct { + name string + input tokenregistry.AssetImage + expected tokenregistry.AssetImage + }{ + { + name: "Png and Svg are local paths", + input: tokenregistry.AssetImage{ + Png: some("./img/asset.png"), + Svg: some("./img/asset.svg"), + }, + expected: tokenregistry.AssetImage{ + Png: some("https://raw.githubusercontent.com/NibiruChain/nibiru/main/token-registry/img/asset.png"), + Svg: some("https://raw.githubusercontent.com/NibiruChain/nibiru/main/token-registry/img/asset.svg"), + }, + }, + { + name: "Png is local, Svg is external", + input: tokenregistry.AssetImage{ + Png: some("./img/asset.png"), + Svg: some("https://example.com/asset.svg"), + }, + expected: tokenregistry.AssetImage{ + Png: some("https://raw.githubusercontent.com/NibiruChain/nibiru/main/token-registry/img/asset.png"), + Svg: some("https://example.com/asset.svg"), + }, + }, + { + name: "Both Png and Svg are external URLs", + input: tokenregistry.AssetImage{ + Png: some("https://example.com/asset.png"), + Svg: some("https://example.com/asset.svg"), + }, + expected: tokenregistry.AssetImage{ + Png: some("https://example.com/asset.png"), + Svg: some("https://example.com/asset.svg"), + }, + }, + { + name: "Both Png and Svg are nil", + input: tokenregistry.AssetImage{ + Png: nil, + Svg: nil, + }, + expected: tokenregistry.AssetImage{ + Png: nil, + Svg: nil, + }, + }, + } + + for _, tt := range tests { + s.Run(tt.name, func() { + result := tt.input.GitHubify() + s.Equal(tt.expected, result) + }) + } +} diff --git a/token-registry/githubify.go b/token-registry/githubify.go new file mode 100644 index 000000000..7bedf6a3d --- /dev/null +++ b/token-registry/githubify.go @@ -0,0 +1,63 @@ +package tokenregistry + +import "strings" + +func (t Token) GitHubify() Token { + out := t + + if out.LogoURIs != nil { + out.LogoURIs = out.LogoURIs.GitHubify() + } + + for imgIdx, img := range out.Images { + out.Images[imgIdx] = img.GitHubify() + } + + return out +} + +// localImageToGitHub converts a path to a local image into a GitHub download +// link in the NibiruChain/nibiru repository. +func localImageToGitHub(local string) string { + trimmed := strings.TrimPrefix(local, "./img/") + return "https://raw.githubusercontent.com/NibiruChain/nibiru/main/token-registry/img/" + trimmed +} + +// isLocalImage returns true if an image URI is meant for a local file in +// the "token-registry/img" directory. +func isLocalImage(maybeLocal *string) bool { + if maybeLocal == nil { + return false + } + return strings.HasPrefix(*maybeLocal, "./img") +} + +func (logouris LogoURIs) GitHubify() *LogoURIs { + out := new(LogoURIs) + out.Png, out.Svg = logouris.Png, logouris.Svg + if logouris.Png != nil && isLocalImage(logouris.Png) { + out.Png = some(localImageToGitHub(*logouris.Png)) + } + if logouris.Svg != nil && isLocalImage(logouris.Svg) { + out.Svg = some(localImageToGitHub(*logouris.Svg)) + } + return out +} + +func (ai AssetImage) GitHubify() AssetImage { + out := AssetImage{} + out.Png, out.Svg = ai.Png, ai.Svg + if ai.Png != nil && isLocalImage(ai.Png) { + out.Png = some(localImageToGitHub(*ai.Png)) + } + if ai.Svg != nil && isLocalImage(ai.Svg) { + out.Svg = some(localImageToGitHub(*ai.Svg)) + } + if ai.Theme != nil { + out.Theme = ai.Theme + } + if ai.ImageSync != nil { + out.ImageSync = ai.ImageSync + } + return out +} diff --git a/token-registry/img/0000_nibiru.png b/token-registry/img/0000_nibiru.png new file mode 100644 index 000000000..46dbd6488 Binary files /dev/null and b/token-registry/img/0000_nibiru.png differ diff --git a/token-registry/img/0000_nibiru.svg b/token-registry/img/0000_nibiru.svg new file mode 100644 index 000000000..15eabe343 --- /dev/null +++ b/token-registry/img/0000_nibiru.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/token-registry/img/0001_stnibi-500x500.png b/token-registry/img/0001_stnibi-500x500.png new file mode 100644 index 000000000..0b0131a3e Binary files /dev/null and b/token-registry/img/0001_stnibi-500x500.png differ diff --git a/token-registry/img/0001_stnibi-500x500.svg b/token-registry/img/0001_stnibi-500x500.svg new file mode 100644 index 000000000..f8e2b3b7a --- /dev/null +++ b/token-registry/img/0001_stnibi-500x500.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/token-registry/img/0002_usdc.png b/token-registry/img/0002_usdc.png new file mode 100644 index 000000000..8c0ffa7b8 Binary files /dev/null and b/token-registry/img/0002_usdc.png differ diff --git a/token-registry/img/0002_usdc.svg b/token-registry/img/0002_usdc.svg new file mode 100644 index 000000000..bcec78210 --- /dev/null +++ b/token-registry/img/0002_usdc.svg @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/token-registry/img/0003_astrovault-axv.png b/token-registry/img/0003_astrovault-axv.png new file mode 100644 index 000000000..561bb4d5e Binary files /dev/null and b/token-registry/img/0003_astrovault-axv.png differ diff --git a/token-registry/img/0003_astrovault-axv.svg b/token-registry/img/0003_astrovault-axv.svg new file mode 100644 index 000000000..7039be027 --- /dev/null +++ b/token-registry/img/0003_astrovault-axv.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/token-registry/img/0004_astrovault-xnibi.svg b/token-registry/img/0004_astrovault-xnibi.svg new file mode 100644 index 000000000..158c3ca12 --- /dev/null +++ b/token-registry/img/0004_astrovault-xnibi.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/token-registry/main/main.go b/token-registry/main/main.go new file mode 100644 index 000000000..385e8e8d5 --- /dev/null +++ b/token-registry/main/main.go @@ -0,0 +1,64 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "path" + "strings" + + "github.com/rs/zerolog/log" + + tokenregistry "github.com/NibiruChain/nibiru/v2/token-registry" +) + +// findRootPath returns the absolute path of the repository root +// This is retrievable with: go list -m -f {{.Dir}} +func findRootPath() (string, error) { + // rootPath, _ := exec.Command("go list -m -f {{.Dir}}").Output() + // This returns the path to the root of the project. + rootPathBz, err := exec.Command("go", "list", "-m", "-f", "{{.Dir}}").Output() + if err != nil { + return "", err + } + rootPath := strings.Trim(string(rootPathBz), "\n") + return rootPath, nil +} + +const SAVE_PATH_ASSETLIST = "dist/assetlist.json" + +func main() { + assetList := tokenregistry.NibiruAssetList() + + prettyBz, err := json.MarshalIndent(assetList, "", " ") + if err != nil { + log.Error().Msg(err.Error()) + return + } + + rootPath, err := findRootPath() + if err != nil { + log.Error().Msg(err.Error()) + return + } + savePath := path.Join(rootPath, SAVE_PATH_ASSETLIST) + + // Create the dist directory if it does not exist + distDirPath := path.Join(rootPath, "dist") + if _, err := os.Stat(distDirPath); os.IsNotExist(err) { + if err := os.Mkdir(distDirPath, 0755); err != nil { + log.Error().Msg(err.Error()) + return + } + } + + perm := os.FileMode(0666) // All can read and write + err = os.WriteFile(savePath, prettyBz, perm) + if err != nil { + log.Error().Msg(err.Error()) + return + } + + fmt.Printf("✅ Generation complete! See %v\n", SAVE_PATH_ASSETLIST) +} diff --git a/token-registry/token.go b/token-registry/token.go new file mode 100644 index 000000000..ee38bcfca --- /dev/null +++ b/token-registry/token.go @@ -0,0 +1,144 @@ +package tokenregistry + +import ( + "encoding/json" +) + +// some: Helper to create pointers for literals +func some[T any](v T) *T { + return &v +} + +type Token struct { + // A short description of the asset + Description string `json:"description"` + // An extended, detailed description of the asset (optional) + ExtendedDescription *string `json:"extended_description,omitempty"` + // Links to social platforms and official websites (optional) + Socials *SocialLinks `json:"socials,omitempty"` + // Units of denomination for the asset + DenomUnits []DenomUnit `json:"denom_units"` + // The base denomination for the asset (canonical name) + Base string `json:"base"` + // The human-readable name of the asset + Name string `json:"name"` + // The display name or symbol used in UI for the asset + Display string `json:"display"` + // The ticker or symbol of the asset + Symbol string `json:"symbol"` + // URIs for the asset's logo in different formats (optional) + LogoURIs *LogoURIs `json:"logo_URIs,omitempty"` + // Unique identifier for the asset on Coingecko (optional) + CoingeckoID *string `json:"coingecko_id,omitempty"` + // An array of image representations for the asset (optional) + Images []AssetImage `json:"images,omitempty"` + // Type of the asset (e.g., "sdk.coin", "ics20", "erc20") + TypeAsset TypeAsset `json:"type_asset"` + // Trace data for the asset (optional, for cross-chain or liquid staking assets) + Traces []Trace `json:"traces,omitempty"` +} + +type AssetList struct { + Schema string `json:"$schema"` + ChainName string `json:"chain_name"` + Assets []Token `json:"assets"` +} + +// String returns a "pretty" JSON version of the [AssetList]. +func (a AssetList) String() string { + jsonBz, _ := json.MarshalIndent(a, "", " ") + return string(jsonBz) +} + +type SocialLinks struct { + Website *string `json:"website,omitempty"` + Twitter *string `json:"twitter,omitempty"` +} + +type DenomUnit struct { + Denom string `json:"denom"` + Exponent int `json:"exponent"` + Aliases []string `json:"aliases,omitempty"` +} + +type LogoURIs struct { + Png *string `json:"png,omitempty"` + Svg *string `json:"svg,omitempty"` +} + +type AssetImage struct { + Png *string `json:"png,omitempty"` + Svg *string `json:"svg,omitempty"` + Theme *ImageTheme `json:"theme,omitempty"` + ImageSync *ImageSync `json:"image_sync,omitempty"` +} + +// ImageTheme represents theme customization for an image. +type ImageTheme struct { + // Whether the image should appear in a circular format (optional) + Circle *bool `json:"circle,omitempty"` + // Primary color in hexadecimal format (optional) + PrimaryColorHex *string `json:"primary_color_hex,omitempty"` +} + +// ImageSync represents synchronization details for cross-chain assets. +type ImageSync struct { + // Name of the chain associated with the image + ChainName string `json:"chain_name"` + // Base denomination of the synced asset + BaseDenom string `json:"base_denom"` +} + +// TypeAsset is an enum type for "type_asset". Valid values include "sdk.coin", +// "ics20", and "erc20". See [Examples]. +// +// [Examples]: https://www.notion.so/nibiru/Nibiru-Token-Registry-Info-Fungible-Tokens-on-Nibiru-cf46d37ccd9c4c33bb083e20e0fa8e20?pvs=4 +type TypeAsset string + +const ( + TypeAsset_SDKCoin TypeAsset = "sdk.coin" + TypeAsset_ICS20 TypeAsset = "ics20" + TypeAsset_ERC20 TypeAsset = "erc20" + TypeAsset_CW20 TypeAsset = "cw20" +) + +// Trace represents trace data for cross-chain or liquid staking assets. +type Trace struct { + // Type of trace (e.g., "ibc", "liquid-stake", "wrapped", "bridge") + Type TraceType `json:"type"` + // Counterparty information for the trace + Counterparty Counterparty `json:"counterparty"` + // Provider of the asset for liquid staking or cross-chain trace (optional) + Provider *string `json:"provider,omitempty"` + // Additional chain-level details (optional) + Chain *TraceChainInfo `json:"chain,omitempty"` +} + +// TraceType is an enum type for "trace.type" (e.g. "ibc", "liquid-stake", +// "wrapped", "bridge") +type TraceType string + +const ( + TraceType_IBC TraceType = "ibc" + TraceType_LiquidStake TraceType = "liquid-stake" + TraceType_Wrapped TraceType = "wrapped" + TraceType_Bridge TraceType = "bridge" +) + +// Counterparty represents the counterparty information for an asset trace. +type Counterparty struct { + // Name of the counterparty chain + ChainName string `json:"chain_name"` + // Base denomination on the counterparty chain + BaseDenom string `json:"base_denom"` + // Channel ID used for communication (optional) + ChannelID *string `json:"channel_id,omitempty"` +} + +// TraceChainInfo represents additional chain-level details for an asset trace. +type TraceChainInfo struct { + // Channel ID on the chain + ChannelID string `json:"channel_id"` + // Path used for asset transfer on the chain + Path string `json:"path"` +} diff --git a/x/evm/const.go b/x/evm/const.go index 1ddf8e67d..0d86ca185 100644 --- a/x/evm/const.go +++ b/x/evm/const.go @@ -6,6 +6,7 @@ import ( "math/big" "github.com/NibiruChain/collections" + sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" gethcommon "github.com/ethereum/go-ethereum/common" ) @@ -83,10 +84,14 @@ const ( CallTypeSmart ) -var EVM_MODULE_ADDRESS gethcommon.Address +var ( + EVM_MODULE_ADDRESS gethcommon.Address + EVM_MODULE_ADDRESS_NIBI sdk.AccAddress +) func init() { - EVM_MODULE_ADDRESS = gethcommon.BytesToAddress(authtypes.NewModuleAddress(ModuleName)) + EVM_MODULE_ADDRESS_NIBI = authtypes.NewModuleAddress(ModuleName) + EVM_MODULE_ADDRESS = gethcommon.BytesToAddress(EVM_MODULE_ADDRESS_NIBI) } // NativeToWei converts a "unibi" amount to "wei" units for the EVM. diff --git a/x/evm/embeds/artifacts/contracts/TestERC20TransferWithFee.sol/TestERC20TransferWithFee.json b/x/evm/embeds/artifacts/contracts/TestERC20TransferWithFee.sol/TestERC20TransferWithFee.json new file mode 100644 index 000000000..a3ad2ea1e --- /dev/null +++ b/x/evm/embeds/artifacts/contracts/TestERC20TransferWithFee.sol/TestERC20TransferWithFee.json @@ -0,0 +1,297 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "TestERC20TransferWithFee", + "sourceName": "contracts/TestERC20TransferWithFee.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x60806040523480156200001157600080fd5b5060405162001c7b38038062001c7b833981810160405281019062000037919062000385565b818181600390816200004a919062000655565b5080600490816200005c919062000655565b50505062000073336103e86200007b60201b60201c565b505062000857565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603620000ed576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401620000e4906200079d565b60405180910390fd5b6200010160008383620001e860201b60201c565b8060026000828254620001159190620007ee565b92505081905550806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508173ffffffffffffffffffffffffffffffffffffffff16600073ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051620001c891906200083a565b60405180910390a3620001e460008383620001ed60201b60201c565b5050565b505050565b505050565b6000604051905090565b600080fd5b600080fd5b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6200025b8262000210565b810181811067ffffffffffffffff821117156200027d576200027c62000221565b5b80604052505050565b600062000292620001f2565b9050620002a0828262000250565b919050565b600067ffffffffffffffff821115620002c357620002c262000221565b5b620002ce8262000210565b9050602081019050919050565b60005b83811015620002fb578082015181840152602081019050620002de565b60008484015250505050565b60006200031e6200031884620002a5565b62000286565b9050828152602081018484840111156200033d576200033c6200020b565b5b6200034a848285620002db565b509392505050565b600082601f8301126200036a576200036962000206565b5b81516200037c84826020860162000307565b91505092915050565b600080604083850312156200039f576200039e620001fc565b5b600083015167ffffffffffffffff811115620003c057620003bf62000201565b5b620003ce8582860162000352565b925050602083015167ffffffffffffffff811115620003f257620003f162000201565b5b620004008582860162000352565b9150509250929050565b600081519050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600060028204905060018216806200045d57607f821691505b60208210810362000473576200047262000415565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b600060088302620004dd7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff826200049e565b620004e986836200049e565b95508019841693508086168417925050509392505050565b6000819050919050565b6000819050919050565b600062000536620005306200052a8462000501565b6200050b565b62000501565b9050919050565b6000819050919050565b620005528362000515565b6200056a62000561826200053d565b848454620004ab565b825550505050565b600090565b6200058162000572565b6200058e81848462000547565b505050565b5b81811015620005b657620005aa60008262000577565b60018101905062000594565b5050565b601f8211156200060557620005cf8162000479565b620005da846200048e565b81016020851015620005ea578190505b62000602620005f9856200048e565b83018262000593565b50505b505050565b600082821c905092915050565b60006200062a600019846008026200060a565b1980831691505092915050565b600062000645838362000617565b9150826002028217905092915050565b62000660826200040a565b67ffffffffffffffff8111156200067c576200067b62000221565b5b62000688825462000444565b62000695828285620005ba565b600060209050601f831160018114620006cd5760008415620006b8578287015190505b620006c4858262000637565b86555062000734565b601f198416620006dd8662000479565b60005b828110156200070757848901518255600182019150602085019450602081019050620006e0565b8683101562000727578489015162000723601f89168262000617565b8355505b6001600288020188555050505b505050505050565b600082825260208201905092915050565b7f45524332303a206d696e7420746f20746865207a65726f206164647265737300600082015250565b600062000785601f836200073c565b915062000792826200074d565b602082019050919050565b60006020820190508181036000830152620007b88162000776565b9050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000620007fb8262000501565b9150620008088362000501565b9250828201905080821115620008235762000822620007bf565b5b92915050565b620008348162000501565b82525050565b600060208201905062000851600083018462000829565b92915050565b61141480620008676000396000f3fe608060405234801561001057600080fd5b50600436106100a95760003560e01c80633950935111610071578063395093511461016857806370a082311461019857806395d89b41146101c8578063a457c2d7146101e6578063a9059cbb14610216578063dd62ed3e14610246576100a9565b806306fdde03146100ae578063095ea7b3146100cc57806318160ddd146100fc57806323b872dd1461011a578063313ce5671461014a575b600080fd5b6100b6610276565b6040516100c39190610b89565b60405180910390f35b6100e660048036038101906100e19190610c44565b610308565b6040516100f39190610c9f565b60405180910390f35b61010461032b565b6040516101119190610cc9565b60405180910390f35b610134600480360381019061012f9190610ce4565b610335565b6040516101419190610c9f565b60405180910390f35b610152610364565b60405161015f9190610d53565b60405180910390f35b610182600480360381019061017d9190610c44565b61036d565b60405161018f9190610c9f565b60405180910390f35b6101b260048036038101906101ad9190610d6e565b6103a4565b6040516101bf9190610cc9565b60405180910390f35b6101d06103ec565b6040516101dd9190610b89565b60405180910390f35b61020060048036038101906101fb9190610c44565b61047e565b60405161020d9190610c9f565b60405180910390f35b610230600480360381019061022b9190610c44565b6104f5565b60405161023d9190610c9f565b60405180910390f35b610260600480360381019061025b9190610d9b565b610595565b60405161026d9190610cc9565b60405180910390f35b60606003805461028590610e0a565b80601f01602080910402602001604051908101604052809291908181526020018280546102b190610e0a565b80156102fe5780601f106102d3576101008083540402835291602001916102fe565b820191906000526020600020905b8154815290600101906020018083116102e157829003601f168201915b5050505050905090565b60008061031361061c565b9050610320818585610624565b600191505092915050565b6000600254905090565b60008061034061061c565b905061034d8582856107ed565b610358858585610879565b60019150509392505050565b60006012905090565b60008061037861061c565b905061039981858561038a8589610595565b6103949190610e6a565b610624565b600191505092915050565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b6060600480546103fb90610e0a565b80601f016020809104026020016040519081016040528092919081815260200182805461042790610e0a565b80156104745780601f1061044957610100808354040283529160200191610474565b820191906000526020600020905b81548152906001019060200180831161045757829003601f168201915b5050505050905090565b60008061048961061c565b905060006104978286610595565b9050838110156104dc576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016104d390610f10565b60405180910390fd5b6104e98286868403610624565b60019250505092915050565b60008061050061061c565b905060008311610545576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161053c90610fa2565b60405180910390fd5b60006064600a856105569190610fc2565b6105609190611033565b9050600081856105709190611064565b905061057d833084610879565b610588838783610879565b6001935050505092915050565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905092915050565b600033905090565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603610693576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161068a9061110a565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610702576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016106f99061119c565b60405180910390fd5b80600160008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925836040516107e09190610cc9565b60405180910390a3505050565b60006107f98484610595565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81146108735781811015610865576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161085c90611208565b60405180910390fd5b6108728484848403610624565b5b50505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036108e8576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016108df9061129a565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610957576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161094e9061132c565b60405180910390fd5b610962838383610aef565b60008060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050818110156109e8576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016109df906113be565b60405180910390fd5b8181036000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef84604051610ad69190610cc9565b60405180910390a3610ae9848484610af4565b50505050565b505050565b505050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610b33578082015181840152602081019050610b18565b60008484015250505050565b6000601f19601f8301169050919050565b6000610b5b82610af9565b610b658185610b04565b9350610b75818560208601610b15565b610b7e81610b3f565b840191505092915050565b60006020820190508181036000830152610ba38184610b50565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610bdb82610bb0565b9050919050565b610beb81610bd0565b8114610bf657600080fd5b50565b600081359050610c0881610be2565b92915050565b6000819050919050565b610c2181610c0e565b8114610c2c57600080fd5b50565b600081359050610c3e81610c18565b92915050565b60008060408385031215610c5b57610c5a610bab565b5b6000610c6985828601610bf9565b9250506020610c7a85828601610c2f565b9150509250929050565b60008115159050919050565b610c9981610c84565b82525050565b6000602082019050610cb46000830184610c90565b92915050565b610cc381610c0e565b82525050565b6000602082019050610cde6000830184610cba565b92915050565b600080600060608486031215610cfd57610cfc610bab565b5b6000610d0b86828701610bf9565b9350506020610d1c86828701610bf9565b9250506040610d2d86828701610c2f565b9150509250925092565b600060ff82169050919050565b610d4d81610d37565b82525050565b6000602082019050610d686000830184610d44565b92915050565b600060208284031215610d8457610d83610bab565b5b6000610d9284828501610bf9565b91505092915050565b60008060408385031215610db257610db1610bab565b5b6000610dc085828601610bf9565b9250506020610dd185828601610bf9565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b60006002820490506001821680610e2257607f821691505b602082108103610e3557610e34610ddb565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000610e7582610c0e565b9150610e8083610c0e565b9250828201905080821115610e9857610e97610e3b565b5b92915050565b7f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f7760008201527f207a65726f000000000000000000000000000000000000000000000000000000602082015250565b6000610efa602583610b04565b9150610f0582610e9e565b604082019050919050565b60006020820190508181036000830152610f2981610eed565b9050919050565b7f5472616e7366657220616d6f756e74206d75737420626520677265617465722060008201527f7468616e207a65726f0000000000000000000000000000000000000000000000602082015250565b6000610f8c602983610b04565b9150610f9782610f30565b604082019050919050565b60006020820190508181036000830152610fbb81610f7f565b9050919050565b6000610fcd82610c0e565b9150610fd883610c0e565b9250828202610fe681610c0e565b91508282048414831517610ffd57610ffc610e3b565b5b5092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b600061103e82610c0e565b915061104983610c0e565b92508261105957611058611004565b5b828204905092915050565b600061106f82610c0e565b915061107a83610c0e565b925082820390508181111561109257611091610e3b565b5b92915050565b7f45524332303a20617070726f76652066726f6d20746865207a65726f2061646460008201527f7265737300000000000000000000000000000000000000000000000000000000602082015250565b60006110f4602483610b04565b91506110ff82611098565b604082019050919050565b60006020820190508181036000830152611123816110e7565b9050919050565b7f45524332303a20617070726f766520746f20746865207a65726f20616464726560008201527f7373000000000000000000000000000000000000000000000000000000000000602082015250565b6000611186602283610b04565b91506111918261112a565b604082019050919050565b600060208201905081810360008301526111b581611179565b9050919050565b7f45524332303a20696e73756666696369656e7420616c6c6f77616e6365000000600082015250565b60006111f2601d83610b04565b91506111fd826111bc565b602082019050919050565b60006020820190508181036000830152611221816111e5565b9050919050565b7f45524332303a207472616e736665722066726f6d20746865207a65726f20616460008201527f6472657373000000000000000000000000000000000000000000000000000000602082015250565b6000611284602583610b04565b915061128f82611228565b604082019050919050565b600060208201905081810360008301526112b381611277565b9050919050565b7f45524332303a207472616e7366657220746f20746865207a65726f206164647260008201527f6573730000000000000000000000000000000000000000000000000000000000602082015250565b6000611316602383610b04565b9150611321826112ba565b604082019050919050565b6000602082019050818103600083015261134581611309565b9050919050565b7f45524332303a207472616e7366657220616d6f756e742065786365656473206260008201527f616c616e63650000000000000000000000000000000000000000000000000000602082015250565b60006113a8602683610b04565b91506113b38261134c565b604082019050919050565b600060208201905081810360008301526113d78161139b565b905091905056fea2646970667358221220a163955cd8b44c46d18ec3c2ccad0a81dbb6f9a839f8fde7ac6328ed63ead16d64736f6c63430008180033", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100a95760003560e01c80633950935111610071578063395093511461016857806370a082311461019857806395d89b41146101c8578063a457c2d7146101e6578063a9059cbb14610216578063dd62ed3e14610246576100a9565b806306fdde03146100ae578063095ea7b3146100cc57806318160ddd146100fc57806323b872dd1461011a578063313ce5671461014a575b600080fd5b6100b6610276565b6040516100c39190610b89565b60405180910390f35b6100e660048036038101906100e19190610c44565b610308565b6040516100f39190610c9f565b60405180910390f35b61010461032b565b6040516101119190610cc9565b60405180910390f35b610134600480360381019061012f9190610ce4565b610335565b6040516101419190610c9f565b60405180910390f35b610152610364565b60405161015f9190610d53565b60405180910390f35b610182600480360381019061017d9190610c44565b61036d565b60405161018f9190610c9f565b60405180910390f35b6101b260048036038101906101ad9190610d6e565b6103a4565b6040516101bf9190610cc9565b60405180910390f35b6101d06103ec565b6040516101dd9190610b89565b60405180910390f35b61020060048036038101906101fb9190610c44565b61047e565b60405161020d9190610c9f565b60405180910390f35b610230600480360381019061022b9190610c44565b6104f5565b60405161023d9190610c9f565b60405180910390f35b610260600480360381019061025b9190610d9b565b610595565b60405161026d9190610cc9565b60405180910390f35b60606003805461028590610e0a565b80601f01602080910402602001604051908101604052809291908181526020018280546102b190610e0a565b80156102fe5780601f106102d3576101008083540402835291602001916102fe565b820191906000526020600020905b8154815290600101906020018083116102e157829003601f168201915b5050505050905090565b60008061031361061c565b9050610320818585610624565b600191505092915050565b6000600254905090565b60008061034061061c565b905061034d8582856107ed565b610358858585610879565b60019150509392505050565b60006012905090565b60008061037861061c565b905061039981858561038a8589610595565b6103949190610e6a565b610624565b600191505092915050565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b6060600480546103fb90610e0a565b80601f016020809104026020016040519081016040528092919081815260200182805461042790610e0a565b80156104745780601f1061044957610100808354040283529160200191610474565b820191906000526020600020905b81548152906001019060200180831161045757829003601f168201915b5050505050905090565b60008061048961061c565b905060006104978286610595565b9050838110156104dc576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016104d390610f10565b60405180910390fd5b6104e98286868403610624565b60019250505092915050565b60008061050061061c565b905060008311610545576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161053c90610fa2565b60405180910390fd5b60006064600a856105569190610fc2565b6105609190611033565b9050600081856105709190611064565b905061057d833084610879565b610588838783610879565b6001935050505092915050565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905092915050565b600033905090565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603610693576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161068a9061110a565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610702576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016106f99061119c565b60405180910390fd5b80600160008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925836040516107e09190610cc9565b60405180910390a3505050565b60006107f98484610595565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81146108735781811015610865576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161085c90611208565b60405180910390fd5b6108728484848403610624565b5b50505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036108e8576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016108df9061129a565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610957576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161094e9061132c565b60405180910390fd5b610962838383610aef565b60008060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050818110156109e8576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016109df906113be565b60405180910390fd5b8181036000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef84604051610ad69190610cc9565b60405180910390a3610ae9848484610af4565b50505050565b505050565b505050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610b33578082015181840152602081019050610b18565b60008484015250505050565b6000601f19601f8301169050919050565b6000610b5b82610af9565b610b658185610b04565b9350610b75818560208601610b15565b610b7e81610b3f565b840191505092915050565b60006020820190508181036000830152610ba38184610b50565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610bdb82610bb0565b9050919050565b610beb81610bd0565b8114610bf657600080fd5b50565b600081359050610c0881610be2565b92915050565b6000819050919050565b610c2181610c0e565b8114610c2c57600080fd5b50565b600081359050610c3e81610c18565b92915050565b60008060408385031215610c5b57610c5a610bab565b5b6000610c6985828601610bf9565b9250506020610c7a85828601610c2f565b9150509250929050565b60008115159050919050565b610c9981610c84565b82525050565b6000602082019050610cb46000830184610c90565b92915050565b610cc381610c0e565b82525050565b6000602082019050610cde6000830184610cba565b92915050565b600080600060608486031215610cfd57610cfc610bab565b5b6000610d0b86828701610bf9565b9350506020610d1c86828701610bf9565b9250506040610d2d86828701610c2f565b9150509250925092565b600060ff82169050919050565b610d4d81610d37565b82525050565b6000602082019050610d686000830184610d44565b92915050565b600060208284031215610d8457610d83610bab565b5b6000610d9284828501610bf9565b91505092915050565b60008060408385031215610db257610db1610bab565b5b6000610dc085828601610bf9565b9250506020610dd185828601610bf9565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b60006002820490506001821680610e2257607f821691505b602082108103610e3557610e34610ddb565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000610e7582610c0e565b9150610e8083610c0e565b9250828201905080821115610e9857610e97610e3b565b5b92915050565b7f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f7760008201527f207a65726f000000000000000000000000000000000000000000000000000000602082015250565b6000610efa602583610b04565b9150610f0582610e9e565b604082019050919050565b60006020820190508181036000830152610f2981610eed565b9050919050565b7f5472616e7366657220616d6f756e74206d75737420626520677265617465722060008201527f7468616e207a65726f0000000000000000000000000000000000000000000000602082015250565b6000610f8c602983610b04565b9150610f9782610f30565b604082019050919050565b60006020820190508181036000830152610fbb81610f7f565b9050919050565b6000610fcd82610c0e565b9150610fd883610c0e565b9250828202610fe681610c0e565b91508282048414831517610ffd57610ffc610e3b565b5b5092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b600061103e82610c0e565b915061104983610c0e565b92508261105957611058611004565b5b828204905092915050565b600061106f82610c0e565b915061107a83610c0e565b925082820390508181111561109257611091610e3b565b5b92915050565b7f45524332303a20617070726f76652066726f6d20746865207a65726f2061646460008201527f7265737300000000000000000000000000000000000000000000000000000000602082015250565b60006110f4602483610b04565b91506110ff82611098565b604082019050919050565b60006020820190508181036000830152611123816110e7565b9050919050565b7f45524332303a20617070726f766520746f20746865207a65726f20616464726560008201527f7373000000000000000000000000000000000000000000000000000000000000602082015250565b6000611186602283610b04565b91506111918261112a565b604082019050919050565b600060208201905081810360008301526111b581611179565b9050919050565b7f45524332303a20696e73756666696369656e7420616c6c6f77616e6365000000600082015250565b60006111f2601d83610b04565b91506111fd826111bc565b602082019050919050565b60006020820190508181036000830152611221816111e5565b9050919050565b7f45524332303a207472616e736665722066726f6d20746865207a65726f20616460008201527f6472657373000000000000000000000000000000000000000000000000000000602082015250565b6000611284602583610b04565b915061128f82611228565b604082019050919050565b600060208201905081810360008301526112b381611277565b9050919050565b7f45524332303a207472616e7366657220746f20746865207a65726f206164647260008201527f6573730000000000000000000000000000000000000000000000000000000000602082015250565b6000611316602383610b04565b9150611321826112ba565b604082019050919050565b6000602082019050818103600083015261134581611309565b9050919050565b7f45524332303a207472616e7366657220616d6f756e742065786365656473206260008201527f616c616e63650000000000000000000000000000000000000000000000000000602082015250565b60006113a8602683610b04565b91506113b38261134c565b604082019050919050565b600060208201905081810360008301526113d78161139b565b905091905056fea2646970667358221220a163955cd8b44c46d18ec3c2ccad0a81dbb6f9a839f8fde7ac6328ed63ead16d64736f6c63430008180033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/x/evm/embeds/contracts/TestERC20TransferWithFee.sol b/x/evm/embeds/contracts/TestERC20TransferWithFee.sol new file mode 100644 index 000000000..e70234e32 --- /dev/null +++ b/x/evm/embeds/contracts/TestERC20TransferWithFee.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract TestERC20TransferWithFee is ERC20 { + uint256 constant FEE_PERCENTAGE = 10; + + constructor(string memory name, string memory symbol) + ERC20(name, symbol) { + _mint(msg.sender, 1000); + } + + function transfer(address to, uint256 amount) public virtual override returns (bool) { + address owner = _msgSender(); + require(amount > 0, "Transfer amount must be greater than zero"); + + uint256 fee = (amount * FEE_PERCENTAGE) / 100; + uint256 recipientAmount = amount - fee; + + _transfer(owner, address(this), fee); + _transfer(owner, to, recipientAmount); + + return true; + } +} diff --git a/x/evm/embeds/embeds.go b/x/evm/embeds/embeds.go index 448d7d2fd..45a130459 100644 --- a/x/evm/embeds/embeds.go +++ b/x/evm/embeds/embeds.go @@ -39,6 +39,8 @@ var ( testPrecompileSelfCallRevertJson []byte //go:embed artifacts/contracts/TestInfiniteRecursionERC20.sol/TestInfiniteRecursionERC20.json testInfiniteRecursionERC20Json []byte + //go:embed artifacts/contracts/TestERC20TransferWithFee.sol/TestERC20TransferWithFee.json + testERC20TransferWithFee []byte ) var ( @@ -126,6 +128,12 @@ var ( Name: "TestInfiniteRecursionERC20.sol", EmbedJSON: testInfiniteRecursionERC20Json, } + // SmartContract_TestERC20TransferWithFee is a test contract + // which simulates malicious ERC20 behavior by adding fee to the transfer() function + SmartContract_TestERC20TransferWithFee = CompiledEvmContract{ + Name: "TestERC20TransferWithFee.sol", + EmbedJSON: testERC20TransferWithFee, + } ) func init() { @@ -141,6 +149,7 @@ func init() { SmartContract_TestERC20TransferThenPrecompileSend.MustLoad() SmartContract_TestPrecompileSelfCallRevert.MustLoad() SmartContract_TestInfiniteRecursionERC20.MustLoad() + SmartContract_TestERC20TransferWithFee.MustLoad() } type CompiledEvmContract struct { diff --git a/x/evm/embeds/embeds_test.go b/x/evm/embeds/embeds_test.go index fab0a6457..d7e7686b2 100644 --- a/x/evm/embeds/embeds_test.go +++ b/x/evm/embeds/embeds_test.go @@ -20,5 +20,6 @@ func TestLoadContracts(t *testing.T) { embeds.SmartContract_TestNativeSendThenPrecompileSendJson.MustLoad() embeds.SmartContract_TestERC20TransferThenPrecompileSend.MustLoad() embeds.SmartContract_TestInfiniteRecursionERC20.MustLoad() + embeds.SmartContract_TestERC20TransferWithFee.MustLoad() }) } diff --git a/x/evm/keeper/funtoken_from_erc20_test.go b/x/evm/keeper/funtoken_from_erc20_test.go index f05fa2fd4..cc5b47d0a 100644 --- a/x/evm/keeper/funtoken_from_erc20_test.go +++ b/x/evm/keeper/funtoken_from_erc20_test.go @@ -452,6 +452,89 @@ func (s *FunTokenFromErc20Suite) TestFunTokenInfiniteRecursionERC20() { s.Require().ErrorContains(err, "execution reverted") } +// TestSendERC20WithFee creates a funtoken from a malicious contract which charges a 10% fee on any transfer. +// Test ensures that after sending ERC20 token to coin and back, all bank coins are burned. +func (s *FunTokenFromErc20Suite) TestSendERC20WithFee() { + deps := evmtest.NewTestDeps() + + s.T().Log("Deploy ERC20") + metadata := keeper.ERC20Metadata{ + Name: "erc20name", + Symbol: "TOKEN", + } + deployResp, err := evmtest.DeployContract( + &deps, embeds.SmartContract_TestERC20TransferWithFee, + metadata.Name, metadata.Symbol, + ) + s.Require().NoError(err) + + s.T().Log("CreateFunToken for the ERC20 with fee") + s.Require().NoError(testapp.FundAccount( + deps.App.BankKeeper, + deps.Ctx, + deps.Sender.NibiruAddr, + deps.EvmKeeper.FeeForCreateFunToken(deps.Ctx), + )) + + resp, err := deps.EvmKeeper.CreateFunToken( + sdk.WrapSDKContext(deps.Ctx), + &evm.MsgCreateFunToken{ + FromErc20: ð.EIP55Addr{ + Address: deployResp.ContractAddr, + }, + Sender: deps.Sender.NibiruAddr.String(), + }, + ) + s.Require().NoError(err, "erc20 %s", deployResp.ContractAddr) + bankDemon := resp.FuntokenMapping.BankDenom + + randomAcc := testutil.AccAddress() + + deps.ResetGasMeter() + + s.T().Log("send erc20 tokens to Bank") + _, err = deps.EvmKeeper.CallContract( + deps.Ctx, + embeds.SmartContract_FunToken.ABI, + deps.Sender.EthAddr, + &precompile.PrecompileAddr_FunToken, + true, + evmtest.FunTokenGasLimitSendToEvm, + "sendToBank", + deployResp.ContractAddr, + big.NewInt(100), + randomAcc.String(), + ) + s.Require().NoError(err) + + s.T().Log("check balances") + evmtest.AssertERC20BalanceEqual(s.T(), deps, deployResp.ContractAddr, deps.Sender.EthAddr, big.NewInt(900)) + evmtest.AssertERC20BalanceEqual(s.T(), deps, deployResp.ContractAddr, deployResp.ContractAddr, big.NewInt(10)) + evmtest.AssertERC20BalanceEqual(s.T(), deps, deployResp.ContractAddr, evm.EVM_MODULE_ADDRESS, big.NewInt(90)) + s.Require().Equal(sdk.NewInt(90), deps.App.BankKeeper.GetBalance(deps.Ctx, randomAcc, bankDemon).Amount) + + deps.ResetGasMeter() + + s.T().Log("send Bank tokens back to erc20") + _, err = deps.EvmKeeper.ConvertCoinToEvm(sdk.WrapSDKContext(deps.Ctx), + &evm.MsgConvertCoinToEvm{ + ToEthAddr: eth.EIP55Addr{ + Address: deps.Sender.EthAddr, + }, + Sender: randomAcc.String(), + BankCoin: sdk.NewCoin(bankDemon, sdk.NewInt(90)), + }, + ) + s.Require().NoError(err) + + s.T().Log("check balances") + evmtest.AssertERC20BalanceEqual(s.T(), deps, deployResp.ContractAddr, deps.Sender.EthAddr, big.NewInt(981)) + evmtest.AssertERC20BalanceEqual(s.T(), deps, deployResp.ContractAddr, deployResp.ContractAddr, big.NewInt(19)) + evmtest.AssertERC20BalanceEqual(s.T(), deps, deployResp.ContractAddr, evm.EVM_MODULE_ADDRESS, big.NewInt(0)) + s.Require().True(deps.App.BankKeeper.GetBalance(deps.Ctx, randomAcc, bankDemon).Amount.Equal(sdk.NewInt(0))) + s.Require().True(deps.App.BankKeeper.GetBalance(deps.Ctx, evm.EVM_MODULE_ADDRESS_NIBI, bankDemon).Amount.Equal(sdk.NewInt(0))) +} + type FunTokenFromErc20Suite struct { suite.Suite } diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go index d88d2e47a..b956a0a85 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -334,18 +334,17 @@ func (k *Keeper) ApplyEvmMsg(ctx sdk.Context, return nil, evmObj, errors.Wrapf(err, "ApplyEvmMsg: invalid wei amount %s", msg.Value()) } + // take over the nonce management from evm: + // - reset sender's nonce to msg.Nonce() before calling evm. + // - increase sender's nonce by one no matter the result. + stateDB.SetNonce(sender.Address(), msg.Nonce()) if contractCreation { - // take over the nonce management from evm: - // - reset sender's nonce to msg.Nonce() before calling evm. - // - increase sender's nonce by one no matter the result. - stateDB.SetNonce(sender.Address(), msg.Nonce()) ret, _, leftoverGas, vmErr = evmObj.Create( sender, msg.Data(), leftoverGas, msgWei, ) - stateDB.SetNonce(sender.Address(), msg.Nonce()+1) } else { ret, leftoverGas, vmErr = evmObj.Call( sender, @@ -355,6 +354,8 @@ func (k *Keeper) ApplyEvmMsg(ctx sdk.Context, msgWei, ) } + // Increment nonce after processing the message + stateDB.SetNonce(sender.Address(), msg.Nonce()+1) // EVM execution error needs to be available for the JSON-RPC client var vmError string @@ -609,14 +610,14 @@ func (k Keeper) convertCoinToEvmBornERC20( // 2 | EVM sends ERC20 tokens to the "to" account. // This should never fail due to the EVM account lacking ERc20 fund because - // the an account must have sent the EVM module ERC20 tokens in the mapping + // the account must have sent the EVM module ERC20 tokens in the mapping // in order to create the coins originally. // // Said another way, if an asset is created as an ERC20 and some amount is // converted to its Bank Coin representation, a balance of the ERC20 is left // inside the EVM module account in order to convert the coins back to // ERC20s. - actualSentAmount, _, err := k.ERC20().Transfer( + _, _, err := k.ERC20().Transfer( erc20Addr, evm.EVM_MODULE_ADDRESS, recipient, @@ -631,8 +632,7 @@ func (k Keeper) convertCoinToEvmBornERC20( // TxMsg, the Bank Coins were minted. Consequently, to preserve an invariant // on the sum of the FunToken's bank and ERC20 supply, we burn the coins here // in the BC → ERC20 conversion. - burnCoin := sdk.NewCoin(coin.Denom, sdk.NewIntFromBigInt(actualSentAmount)) - err = k.Bank.BurnCoins(ctx, evm.ModuleName, sdk.NewCoins(burnCoin)) + err = k.Bank.BurnCoins(ctx, evm.ModuleName, sdk.NewCoins(coin)) if err != nil { return nil, errors.Wrap(err, "failed to burn coins") } @@ -642,7 +642,7 @@ func (k Keeper) convertCoinToEvmBornERC20( Sender: sender.String(), Erc20ContractAddress: funTokenMapping.Erc20Addr.String(), ToEthAddr: recipient.String(), - BankCoin: burnCoin, + BankCoin: coin, }) return &evm.MsgConvertCoinToEvmResponse{}, nil diff --git a/x/evm/statedb/journal_test.go b/x/evm/statedb/journal_test.go index e1260b819..058c09542 100644 --- a/x/evm/statedb/journal_test.go +++ b/x/evm/statedb/journal_test.go @@ -156,10 +156,10 @@ func (s *Suite) TestComplexJournalChanges() { stateDB, ok = evmObj.StateDB.(*statedb.StateDB) s.Require().True(ok, "error retrieving StateDB from the EVM") - s.T().Log("Expect exactly 0 dirty journal entry for the precompile snapshot") - if stateDB.DebugDirtiesCount() != 0 { + s.T().Log("Expect exactly 1 dirty journal entry for the precompile snapshot") + if stateDB.DebugDirtiesCount() != 1 { debugDirtiesCountMismatch(stateDB, s.T()) - s.FailNow("expected 0 dirty journal changes") + s.FailNow("expected 1 dirty journal change") } s.T().Log("Expect no change since the StateDB has not been committed")