[Bug] x/wasmbinding SetTokenMetadata doesn't set top-level Metadata.Decimals, breaking auto-deployed ERC20 decimals() for adapter-wrapped denoms
Summary
The CosmWasm SetTokenMetadata wasm-binding message translates to a tokenfactory.MsgSetDenomMetadata whose Metadata struct omits the top-level Decimals field. The bank precompile (used by every auto-deployed MintBurnBankERC20 / FixedSupplyBankERC20) reads metadata.Decimals directly when serving decimals(). Result: any ERC20 pair auto-deployed for a tokenfactory denom whose metadata was set via the wasm binding reports decimals() == 0 regardless of the actual decimal count.
This is a one-line oversight in injective-chain/wasmbinding/message_plugin.go that diverged from the precompile when Injective extended the bank Metadata proto with a top-level Decimals field.
Code locations
Bug — injective-chain/wasmbinding/message_plugin.go (lines 140–160 of release/v1.19.x):
case contractMsg.SetTokenMetadata != nil:
mt := contractMsg.SetTokenMetadata
sdkMsg = &tokenfactorytypes.MsgSetDenomMetadata{
Sender: contractAddr.String(),
Metadata: banktypes.Metadata{
Base: mt.Denom,
Display: mt.Symbol,
Name: mt.Name,
Symbol: mt.Symbol,
// ← Decimals field is never set
DenomUnits: []*banktypes.DenomUnit{
{ Denom: mt.Denom, Exponent: 0 },
{ Denom: mt.Symbol, Exponent: mt.Decimals },
},
},
}
Conflicting read — injective-chain/modules/evm/precompiles/bank/bank.go (line 320 of release/v1.19.x):
metadata, _ := bc.bankKeeper.GetDenomMetaData(stateDB.CacheContext(), denom)
return method.Outputs.Pack(metadata.Name, metadata.Symbol, uint8(metadata.Decimals))
The precompile reads metadata.Decimals (top-level), not DenomUnits[i].Exponent.
Evidence (mainnet, 2026-05-27)
Two mainnet denoms have populated metadata set via the immutable CW20 Adapter inj14ejqjyq8um4p3xfqj74yld5waqljf88f9eneuk by calling UpdateMetadata. Both show the same broken pattern:
| denom |
display |
display-unit exponent |
top-level Decimals |
factory/inj14ej…/inj1300xcg9…87xm8 (SHROOM / shroomin CW20) |
SHROOM |
18 |
0 |
factory/inj14ej…/inj1x997dy…zcnq (TRUINJ) |
TRUINJ |
18 |
0 |
For comparison, a denom whose metadata was set via direct MsgSetDenomMetadata (proposal 594, $DRUGS):
| denom |
display |
display-unit exponent |
top-level Decimals |
deployed ERC20 decimals() |
factory/inj178zy…/DRUGS |
DRUGS |
6 |
6 |
6 (verified via EVM RPC at 0x7F073F52051eEF8512519ee7A627F8db1cD63303) |
So the precompile path works correctly when metadata.Decimals is populated; the bug is strictly in the wasm-binding translation that fails to populate it.
Impact
Every CW20-adapter-wrapped tokenfactory denom (197 denoms registered today via the standard inj14ej… adapter) is affected as soon as the project tries to MTS-register the bank denom. The wrapper ERC20 would report decimals() == 0, breaking DEX pricing, balance display, and any other EVM-side use.
Existing workarounds are all blocked:
- Direct
MsgSetDenomMetadata with the new proto requires admin signature; admin is the non-signing CW20 Adapter contract.
- Gov-bypass
MsgSetDenomMetadata only activates when admin == ""; the adapter's denoms have admin set.
- Adapter migration is impossible: the adapter contract has
admin: "" (wasmd-level), so MsgMigrateContract cannot succeed.
- Adapter
ChangeAdmin isn't exposed in the adapter's ExecuteMsg enum, so denom admin can't be transferred away.
Without a chain-level fix, every project using the standard CW20 Adapter is locked out of functional MTS registration.
Proposed fix
One-line addition in message_plugin.go SetTokenMetadata branch:
Metadata: banktypes.Metadata{
Base: mt.Denom,
Display: mt.Symbol,
Name: mt.Name,
Symbol: mt.Symbol,
+ Decimals: mt.Decimals,
DenomUnits: []*banktypes.DenomUnit{
{ Denom: mt.Denom, Exponent: 0 },
{ Denom: mt.Symbol, Exponent: mt.Decimals },
},
},
The value is already correctly propagated as the second DenomUnit.Exponent. This fix surfaces the same value as the top-level field the precompile reads.
Compatibility / safety
- Existing denoms unaffected by the patch. They keep their current
Decimals=0 until UpdateMetadata is re-run on them after the fix lands.
- Validation pass.
tokenfactory.MsgServer.SetDenomMetadata enforces existingMetadata.Decimals != 0 && existingMetadata.Decimals != msg.Metadata.Decimals → first-time set or matching-set both pass.
- Bank
Metadata.Validate() accepts the field — the extended Decimals isn't restricted.
Will follow up with a PR.
Reproduction script
For anyone investigating, this injectived snippet reproduces the divergence (mainnet):
# Query SHROOM bank metadata — note `decimals: 0` despite display-unit exponent 18
injectived q bank denom-metadata factory/inj14ejqjyq8um4p3xfqj74yld5waqljf88f9eneuk/inj1300xcg9naqy00fujsr9r8alwk7dh65uqu87xm8 \
--node https://sentry.tm.injective.network:443
# Compare with $DRUGS (set via direct MsgSetDenomMetadata, decimals=6 populated)
injectived q bank denom-metadata factory/inj178zy7myyxewek7ka7v9hru8ycpvfnen6xeps89/DRUGS \
--node https://sentry.tm.injective.network:443
[Bug]
x/wasmbindingSetTokenMetadatadoesn't set top-levelMetadata.Decimals, breaking auto-deployed ERC20decimals()for adapter-wrapped denomsSummary
The CosmWasm
SetTokenMetadatawasm-binding message translates to atokenfactory.MsgSetDenomMetadatawhoseMetadatastruct omits the top-levelDecimalsfield. The bank precompile (used by every auto-deployedMintBurnBankERC20/FixedSupplyBankERC20) readsmetadata.Decimalsdirectly when servingdecimals(). Result: any ERC20 pair auto-deployed for a tokenfactory denom whose metadata was set via the wasm binding reportsdecimals() == 0regardless of the actual decimal count.This is a one-line oversight in
injective-chain/wasmbinding/message_plugin.gothat diverged from the precompile when Injective extended the bankMetadataproto with a top-levelDecimalsfield.Code locations
Bug —
injective-chain/wasmbinding/message_plugin.go(lines 140–160 ofrelease/v1.19.x):Conflicting read —
injective-chain/modules/evm/precompiles/bank/bank.go(line 320 ofrelease/v1.19.x):The precompile reads
metadata.Decimals(top-level), notDenomUnits[i].Exponent.Evidence (mainnet, 2026-05-27)
Two mainnet denoms have populated metadata set via the immutable CW20 Adapter
inj14ejqjyq8um4p3xfqj74yld5waqljf88f9eneukby callingUpdateMetadata. Both show the same broken pattern:Decimalsfactory/inj14ej…/inj1300xcg9…87xm8(SHROOM / shroomin CW20)SHROOMfactory/inj14ej…/inj1x997dy…zcnq(TRUINJ)TRUINJFor comparison, a denom whose metadata was set via direct
MsgSetDenomMetadata(proposal 594,$DRUGS):Decimalsdecimals()factory/inj178zy…/DRUGSDRUGS0x7F073F52051eEF8512519ee7A627F8db1cD63303)So the precompile path works correctly when
metadata.Decimalsis populated; the bug is strictly in the wasm-binding translation that fails to populate it.Impact
Every CW20-adapter-wrapped tokenfactory denom (197 denoms registered today via the standard
inj14ej…adapter) is affected as soon as the project tries to MTS-register the bank denom. The wrapper ERC20 would reportdecimals() == 0, breaking DEX pricing, balance display, and any other EVM-side use.Existing workarounds are all blocked:
MsgSetDenomMetadatawith the new proto requires admin signature; admin is the non-signing CW20 Adapter contract.MsgSetDenomMetadataonly activates whenadmin == ""; the adapter's denoms have admin set.admin: ""(wasmd-level), soMsgMigrateContractcannot succeed.ChangeAdminisn't exposed in the adapter'sExecuteMsgenum, so denom admin can't be transferred away.Without a chain-level fix, every project using the standard CW20 Adapter is locked out of functional MTS registration.
Proposed fix
One-line addition in
message_plugin.goSetTokenMetadata branch:Metadata: banktypes.Metadata{ Base: mt.Denom, Display: mt.Symbol, Name: mt.Name, Symbol: mt.Symbol, + Decimals: mt.Decimals, DenomUnits: []*banktypes.DenomUnit{ { Denom: mt.Denom, Exponent: 0 }, { Denom: mt.Symbol, Exponent: mt.Decimals }, }, },The value is already correctly propagated as the second
DenomUnit.Exponent. This fix surfaces the same value as the top-level field the precompile reads.Compatibility / safety
Decimals=0untilUpdateMetadatais re-run on them after the fix lands.tokenfactory.MsgServer.SetDenomMetadataenforcesexistingMetadata.Decimals != 0 && existingMetadata.Decimals != msg.Metadata.Decimals→ first-time set or matching-set both pass.Metadata.Validate()accepts the field — the extended Decimals isn't restricted.Will follow up with a PR.
Reproduction script
For anyone investigating, this
injectivedsnippet reproduces the divergence (mainnet):