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")