Skip to main content
The PreciseBank module (x/precisebank) extends the precision of the standard Cosmos SDK bank module from 6 decimals to 18 decimals, enabling full EVM compatibility while maintaining Cosmos coin integrity. This module is required for chains using non-18-decimal native tokens. Big thanks to the Kava team for their valuable contributions to this module.

Module Overview

Purpose: Bridge the decimal precision gap between Cosmos (typically 6 decimals) and EVM (18 decimals) Key Functionality:
  • Extends token precision without changing the base denomination
  • Tracks fractional balances (sub-atomic units) separate from integer balances
  • Maintains 1:1 backing between fractional and integer units
  • Transparent to users - balances appear as expected in both environments
  • Wraps x/bank to provide 18-decimal precision for x/vm
Source Code: x/precisebank Documentation: x/precisebank/README.md

When Do You Need PreciseBank?

You NEED PreciseBank if:

Your native token has 6 decimals (or any non-18 decimal count):
  • Base denom: ustake, utoken, uatom (micro prefix = 10^6)
  • Display denom: stake, token, atom
  • Example: 1 STAKE = 1,000,000 ustake = 10^6 smallest units
Why: EVM expects 18 decimals. Without PreciseBank, you lose 12 decimals of precision, causing rounding errors and broken DeFi protocols.

You DON’T NEED PreciseBank if:

Your native token has 18 decimals:
  • Base denom: atest, atoken (atto prefix = 10^18)
  • Display denom: test, token
  • Example: 1 TEST = 1,000,000,000,000,000,000 atest = 10^18 smallest units
Why: Your Cosmos denomination already matches EVM’s 18-decimal expectation. Direct 1:1 mapping with no fractional tracking needed.

Mathematical Foundation

The Precision Problem

Cosmos Standard: 6 decimal places
EVM Standard: 18 decimal places
Gap: 12 orders of magnitude (10^12)

PreciseBank Solution

PreciseBank subdivides each uatom into 10^12 sub-atomic units called aatom:
Key Principle: Every aatom is fully backed by uatom in x/bank. You cannot have fractional aatom without corresponding integer uatom reserves.

Balance Representation

For any account n, the total balance in sub-atomic units a(n) is: a(n)=b(n)C+f(n)a(n) = b(n) \cdot C + f(n) Where:
  • a(n) = Total aatom balance (18-decimal representation)
  • b(n) = Integer uatom balance (stored in x/bank)
  • f(n) = Fractional balance (stored in x/precisebank)
  • C = Conversion factor = 10^12
Constraints:
Derivation (quotient-remainder theorem):
Example:
Source: README.md Background

Module Integration

Adding to Your Chain

PreciseBank requires integration in app/app.go: 1. Import the module:
2. Add keeper to App struct:
3. Initialize keeper (before VM keeper):
4. Pass PreciseBankKeeper to VM module:
5. Add to module manager:
Critical: PreciseBank must wrap BankKeeper and be passed to VMKeeper, not BankKeeper directly.

Configuration

Genesis Configuration

PreciseBank has minimal genesis configuration - it primarily tracks state, not parameters. File Location: ~/.evmd/config/genesis.json under app_state.precisebank Structure:

Required VM Module Configuration

When using PreciseBank, you MUST configure extended_denom_options in the VM module:
Explanation:
  • native_denom: 6-decimal Cosmos denom (ustake)
  • extended_denom: 18-decimal EVM denom (astake)
  • Conversion: 1 ustake = 10^12 astake
Naming Pattern:
  • u prefix (micro, 10^6) → a prefix (atto, 10^18): ustakeastake
  • Other prefixes → add evm prefix: stakeevmstake

State

fractional_balances

What It Stores: The fractional (sub-atomic) portion of each account’s balance that cannot be represented as whole integer units. Type: Array of FractionalBalance objects Structure (fractional_balance.go:43-48):
Validation (fractional_balance.go:64-78):
  • Amount must be positive (amount > 0)
  • Amount must be less than conversion factor (amount < 10^12)
  • Address must be valid Bech32
Example:
Storage Key: keys.go:17

remainder

What It Stores: A module-level reserve balance that backs all fractional units in circulation. Type: Integer (sdkmath.Int) Purpose: Maintains invariant that total fractional balances equal the module reserve Invariant: remainder=nAf(n)\text{remainder} = \sum_{n \in \mathcal{A}} f(n) Where:
  • remainder = Module reserve in fractional units
  • f(n)\sum f(n) = Sum of all account fractional balances
Why Needed: Since fractional units aren’t tracked in x/bank’s total supply, this reserve account holds integer units to back them. When fractional balances sum to 10^12, one integer unit is held in reserve. Example:
Storage Key: keys.go:22
Source: remainder_amount.go

Operations

Transfer

When transferring fractional amounts, PreciseBank handles the complexity automatically: Example Transfer: Alice sends 1.5 ustake + 500 billion aatom to Bob
Complex Transfer: Alice sends 1,234,567,890,123 aatom to Bob
Source: send.go

Mint

Operation: Create new fractional units
Process:
  1. Split amount into integer and fractional parts
  2. Mint integer part via x/bank
  3. Update fractional balance in x/precisebank
  4. Update remainder to maintain backing invariant
Source: mint.go

Burn

Operation: Destroy fractional units
Process:
  1. Split amount into integer and fractional parts
  2. Burn integer part via x/bank
  3. Update fractional balance in x/precisebank
  4. Update remainder to maintain backing invariant
Source: burn.go

Keeper Interface

PreciseBank implements the full BankKeeper interface, making it a drop-in replacement: Source: keeper.go:16
Key Methods:
  • SendCoins(ctx, from, to, coins) - Transfer with fractional precision
  • MintCoins(ctx, module, coins) - Create new fractional units
  • BurnCoins(ctx, module, coins) - Destroy fractional units
  • GetBalance(ctx, addr, denom) - Get extended balance (integer + fractional)
  • SpendableCoins(ctx, addr) - Get spendable balances with fractional precision
Passthrough Methods: Methods not requiring fractional logic delegate directly to x/bank (keeper.go:44-50):
  • GetSupply() - Total supply
  • IterateTotalSupply() - Supply iteration

Queries

gRPC Queries

Query Fractional Balance:
Query Total Fractional Balances:
Query Remainder:
Source: grpc_query.go

EVM Integration

In Solidity Contracts

From the EVM perspective, users interact with the extended denomination:
Behind the Scenes:
  • transfer(recipient, 1.5e18 astake)
  • PreciseBank: Transfers 1 ustake via x/bank + 500,000,000,000 aatom fractional
  • User sees seamless 18-decimal precision

Events

PreciseBank emits events for fractional balance changes:

SendCoins Event

MintCoins Event

BurnCoins Event

Source: events.go

Common Issues and Solutions

Issue: “Fractional amount exceeds conversion factor”

Symptom: Transaction fails with fractional validation error Cause: Fractional balance >= 10^12 (should have been converted to integer unit) Solution: This indicates a bug in the keeper logic. Report to Cosmos EVM team.

Issue: Balances Don’t Match Between Cosmos/EVM

Symptom: User sees different balance in Cosmos vs MetaMask Cause:
  • PreciseBank not integrated correctly in app.go
  • VM module not using PreciseBankKeeper
  • Missing extended_denom_options configuration
Solution:

Issue: Chain Won’t Start After Adding PreciseBank

Symptom: Genesis validation fails Cause: Missing extended_denom_options in VM params Solution: Add to genesis.json:

Issue: Total Supply Mismatch

Symptom: Sum of balances doesn’t equal total supply Cause: Remainder not properly maintained Solution: Query remainder and verify:

Testing and Verification

Verify Integration

1. Check module is loaded:
2. Query remainder (should be 0 at genesis):
3. Send fractional amount via EVM:
4. Verify invariant:

Performance Considerations

Storage: Fractional balances add one storage entry per account with non-zero fractional amount Gas Cost: Fractional operations add minimal gas overhead (~5-10% more than standard bank operations) Scaling: Module has been tested with millions of accounts, no performance degradation Optimization: Fractional balances are only created when needed. Transfers of exact integer amounts don’t create fractional entries.

Source Code References