Summary
When MsgDeployFungibleCoinZRC20 omits liquidity_cap, the default is computed as DefaultLiquidityCap (1000) * decimals instead of DefaultLiquidityCap * 10^decimals as documented on the constant. The resulting cap is 18000 wei for an 18-decimal token (e.g. ETH), 6000 wei for a 6-decimal token (e.g. USDC), and 8000 wei for an 8-decimal token (e.g. WBTC). getAndCheckZRC20 rejects any non-dust deposit with ErrForeignCoinCapReached, effectively bricking the token until an operational-policy tx resets the cap.
Root cause
x/fungible/keeper/evm.go:172-175:
if liquidityCap == nil {
liquidityCap = ptr.Ptr(sdkmath.NewUint(types.DefaultLiquidityCap).MulUint64(uint64(newCoin.Decimals)))
}
newCoin.Decimals is the raw decimal count, not 10^decimals. The comment on DefaultLiquidityCap in x/fungible/types/zrc20.go:5-8 says the on-chain value should be value * 10^decimals.
Reachability
The default-cap branch is only reachable through MsgDeployFungibleCoinZRC20, which is in AdminPolicyMessages (x/authority/types/authorization_list.go:36). MsgWhitelistAsset already wraps the cap via ptr.Ptr(msg.LiquidityCap) before calling the keeper, so it never hits the buggy branch. The bug is admin-error class, recoverable via MsgUpdateZRC20LiquidityCap (operational policy).
Fix
Mirror the existing big.Int.Exp(10, decimals) pattern from gas_coin_and_pool.go:84:
if liquidityCap == nil {
tenPow := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(newCoin.Decimals)), nil)
liquidityCap = ptr.Ptr(sdkmath.NewUintFromBigInt(tenPow).MulUint64(types.DefaultLiquidityCap))
}
Add a unit test that exercises DeployZRC20Contract with liquidityCap == nil for decimals 6, 8, and 18 to lock in the correct default and prevent regression.
Optional hardening: tighten MsgDeployFungibleCoinZRC20.ValidateBasic to reject LiquidityCap == nil outright.
History
Introduced in commit 367e4aa4f (Cosmos v0.50 upgrade, PR #3357, 2025-01-24). Has been in main since January 2025; the default-cap branch is untested.
Severity
Low. Admin-gated, recoverable in one transaction by the same policy group, no permanent fund loss under normal operation.
References
Summary
When
MsgDeployFungibleCoinZRC20omitsliquidity_cap, the default is computed asDefaultLiquidityCap (1000) * decimalsinstead ofDefaultLiquidityCap * 10^decimalsas documented on the constant. The resulting cap is18000wei for an 18-decimal token (e.g. ETH),6000wei for a 6-decimal token (e.g. USDC), and8000wei for an 8-decimal token (e.g. WBTC).getAndCheckZRC20rejects any non-dust deposit withErrForeignCoinCapReached, effectively bricking the token until an operational-policy tx resets the cap.Root cause
x/fungible/keeper/evm.go:172-175:newCoin.Decimalsis the raw decimal count, not10^decimals. The comment onDefaultLiquidityCapinx/fungible/types/zrc20.go:5-8says the on-chain value should bevalue * 10^decimals.Reachability
The default-cap branch is only reachable through
MsgDeployFungibleCoinZRC20, which is inAdminPolicyMessages(x/authority/types/authorization_list.go:36).MsgWhitelistAssetalready wraps the cap viaptr.Ptr(msg.LiquidityCap)before calling the keeper, so it never hits the buggy branch. The bug is admin-error class, recoverable viaMsgUpdateZRC20LiquidityCap(operational policy).Fix
Mirror the existing
big.Int.Exp(10, decimals)pattern fromgas_coin_and_pool.go:84:Add a unit test that exercises
DeployZRC20ContractwithliquidityCap == nilfor decimals 6, 8, and 18 to lock in the correct default and prevent regression.Optional hardening: tighten
MsgDeployFungibleCoinZRC20.ValidateBasicto rejectLiquidityCap == niloutright.History
Introduced in commit
367e4aa4f(Cosmos v0.50 upgrade, PR #3357, 2025-01-24). Has been inmainsince January 2025; the default-cap branch is untested.Severity
Low. Admin-gated, recoverable in one transaction by the same policy group, no permanent fund loss under normal operation.
References
hackeproof/analysis/blockchain/report_ZCNode-263.md