PreciseBank 模块(x/precisebank)将标准 Cosmos SDK bank 模块的精度从 6 位小数扩展到 18 位小数,在保持 Cosmos coin 完整性的同时实现完整的 EVM 兼容性。对于使用非 18 位小数原生代币的链,此模块是必需的。 特别感谢 Kava 团队为该模块做出的宝贵贡献。

模块概览

用途:弥合 Cosmos(通常为 6 位小数)与 EVM(18 位小数)之间的小数精度差距 核心功能:
  • 在不改变基础面额的情况下扩展代币精度
  • 将小数余额(亚原子单位)与整数余额分开跟踪
  • 保持小数单位与整数单位之间 1:1 的储备支撑
  • 对用户透明,余额在两个环境中的显示都符合预期
  • 为 x/vm 包装 x/bank,提供 18 位小数精度
源代码: x/precisebank 文档: x/precisebank/README.md

何时需要 PreciseBank?

如果满足以下情况,你需要 PreciseBank:

你的原生代币有 6 位小数(或任何非 18 位小数位数):
  • 基础面额:ustake、utoken、uatom(micro 前缀 = 10^6)
  • 展示面额:stake、token、atom
  • 示例:1 STAKE = 1,000,000 ustake = 10^6 个最小单位
原因:EVM 期望 18 位小数。没有 PreciseBank,你将损失 12 位小数精度,导致舍入误差并破坏 DeFi 协议。

如果满足以下情况,你不需要 PreciseBank:

你的原生代币有 18 位小数:
  • 基础面额:atest、atoken(atto 前缀 = 10^18)
  • 展示面额:test、token
  • 示例:1 TEST = 1,000,000,000,000,000,000 atest = 10^18 个最小单位
原因:你的 Cosmos 面额已经与 EVM 对 18 位小数的要求一致。可以直接 1:1 映射,无需跟踪小数部分。

数学基础

精度问题

Cosmos 标准:6 位小数
1 ATOM = 1,000,000 uatom (10^6)
最小单位:0.000001 ATOM = 1 uatom
EVM 标准:18 位小数
1 ETH = 1,000,000,000,000,000,000 wei (10^18)
最小单位:0.000000000000000001 ETH = 1 wei
差距:12 个数量级(10^12)

PreciseBank 方案

PreciseBank 将每个 uatom 细分为 10^12 个称为 aatom 的亚原子单位:
1 ATOM = 1,000,000 uatom (Cosmos 层 - x/bank)
1 uatom = 1,000,000,000,000 aatom (EVM 层 - x/precisebank)
1 ATOM = 1,000,000,000,000,000,000 aatom (总计 10^18)
关键原则:每个 aatom 都由 x/bank 中的 uatom 完整支撑。没有对应的整数 uatom 储备,就不能存在小数 aatom。

余额表示

对于任意账户 n,其以亚原子单位表示的总余额 a(n) 为: a(n)=b(n)⋅C+f(n)a(n) = b(n) \cdot C + f(n) 其中:
  • a(n) = aatom 总余额(18 位小数表示)
  • b(n) = 整数 uatom 余额(存储在 x/bank 中)
  • f(n) = 小数余额(存储在 x/precisebank 中)
  • C = 转换因子 = 10^12
约束:
0 ≤ f(n) < C
a(n), b(n) ≥ 0
推导(商余定理):
b(n) = ⌊a(n) / C⌋  (整数除法)
f(n) = a(n) mod C   (余数)
示例:
用户拥有:1,500,000,123,456,789,012 aatom
b(n) = ⌊1,500,000,123,456,789,012 / 10^12⌋ = 1,500,000 uatom(在 x/bank 中)
f(n) = 1,500,000,123,456,789,012 mod 10^12 = 123,456,789,012 aatom(在 x/precisebank 中)
来源: README.md Background

模块集成

添加到你的链中

PreciseBank 需要集成到 app/app.go 中: 1. 导入模块:
import (
    precisebankkeeper "github.com/cosmos/evm/x/precisebank/keeper"
    precisebanktypes "github.com/cosmos/evm/x/precisebank/types"
)
2. 将 keeper 添加到 App 结构体:
type App struct {
    // ... 其他 keeper ...
    BankKeeper    bankkeeper.Keeper
    PreciseBankKeeper precisebankkeeper.Keeper
    // ... 其他 keeper ...
}
3. 初始化 keeper(在 VM keeper 之前):
// 创建包装 bank keeper 的 precisebank keeper
app.PreciseBankKeeper = precisebankkeeper.NewKeeper(
    appCodec,
    keys[precisebanktypes.StoreKey],
    app.BankKeeper,        // 被包装的 bank keeper
    app.AccountKeeper,
)
4. 将 PreciseBankKeeper 传递给 VM 模块:
// VM keeper 需要 precisebank 来处理 18 位小数操作
app.VMKeeper = vmkeeper.NewKeeper(
    appCodec,
    keys[vmtypes.StoreKey],
    app.PreciseBankKeeper,  // 使用 precisebank 而不是 bank
    app.StakingKeeper,
    // ... 其他 keeper ...
)
5. 添加到模块管理器:
app.ModuleManager = module.NewManager(
    // ... 其他模块 ...
    precisebank.NewAppModule(app.PreciseBankKeeper),
    // ... 其他模块 ...
)
关键:PreciseBank 必须包装 BankKeeper,并传递给 VMKeeper,而不是直接传递 BankKeeper。

配置

Genesis 配置

PreciseBank 的 genesis 配置非常少,它主要跟踪状态而不是参数。 文件位置:~/.evmd/config/genesis.json 中的 app_state.precisebank 结构:
{
  "app_state": {
    "precisebank": {
      "fractional_balances": [],
      "remainder": "0"
    }
  }
}

必需的 VM 模块配置

使用 PreciseBank 时,你必须在 VM 模块中配置 extended_denom_options:
{
  "app_state": {
    "vm": {
      "params": {
        "evm_denom": "ustake",
        "extended_denom_options": [
          {
            "native_denom": "ustake",
            "extended_denom": "astake"
          }
        ]
      }
    }
  }
}
说明:
  • native_denom:6 位小数的 Cosmos 面额(ustake)
  • extended_denom:18 位小数的 EVM 面额(astake)
  • 转换关系:1 ustake = 10^12 astake
命名模式:
  • u 前缀(micro,10^6)→ a 前缀(atto,10^18):ustake → astake
  • 其他前缀 → 添加 evm 前缀:stake → evmstake

状态

fractional_balances

存储内容:每个账户余额中无法表示为完整整数单位的小数(亚原子)部分。 类型:FractionalBalance 对象数组 结构(fractional_balance.go:43-48):
message FractionalBalance {
  string address = 1;  // Bech32 账户地址
  string amount = 2;   // 小数金额(0 < amount < 10^12)
}
校验规则(fractional_balance.go:64-78):
  • 金额必须为正(amount > 0)
  • 金额必须小于转换因子(amount < 10^12)
  • 地址必须是有效的 Bech32
示例:
{
  "fractional_balances": [
    {
      "address": "cosmos1abc...",
      "amount": "123456789012"
    },
    {
      "address": "cosmos1def...",
      "amount": "999999999999"
    }
  ]
}
存储键: keys.go:17
FractionalBalancePrefix = []byte{0x01}
FractionalBalanceKey(address) = address.Bytes()

remainder

存储内容:模块级储备余额,为流通中的所有小数单位提供支撑。 类型:整数(sdkmath.Int) 作用:维持“所有小数余额之和等于模块储备”的不变量 不变量: remainder=∑n∈Af(n)\text{remainder} = \sum_{n \in \mathcal{A}} f(n) 其中:
  • remainder = 以小数单位表示的模块储备
  • ∑f(n)\sum f(n) = 所有账户小数余额之和
为什么需要:由于小数单位不会计入 x/bank 的总供应量,因此这个储备账户会持有整数单位为其提供支撑。当小数余额之和达到 10^12 时,就会有 1 个整数单位作为储备被持有。 示例:
账户 1 小数部分:400,000,000,000 aatom
账户 2 小数部分:600,000,000,000 aatom
小数总和:1,000,000,000,000 aatom = 1 ustake

模块储备:在 x/bank 中持有 1 ustake,为这些小数单位提供支撑
precisebank 中的 remainder:1,000,000,000,000 aatom
存储键: keys.go:22
RemainderBalanceKey = []byte{0x02}
来源: remainder_amount.go

操作

转账

在转移小数金额时,PreciseBank 会自动处理其中的复杂性: 示例转账:爱丽丝向鲍勃发送 1.5 ustake + 5000 亿 aatom
爱丽丝初始:
  x/bank: 10 ustake
  x/precisebank: 500,000,000,000 aatom
  总计:10,500,000,000,000 aatom

鲍勃初始:
  x/bank: 5 ustake
  x/precisebank: 300,000,000,000 aatom
  总计:5,300,000,000,000 aatom

转账金额:2,000,000,000,000 aatom
  = 2 ustake + 0 aatom 小数部分

转账后:
爱丽丝:
  x/bank: 8 ustake (10 - 2)
  x/precisebank: 500,000,000,000 aatom(不变 - 小数部分无变化)
  总计:8,500,000,000,000 aatom

鲍勃:
  x/bank: 7 ustake (5 + 2)
  x/precisebank: 300,000,000,000 aatom(不变)
  总计:7,300,000,000,000 aatom
复杂转账:爱丽丝向鲍勃发送 1,234,567,890,123 aatom
转账:1,234,567,890,123 aatom
  = 1 ustake + 234,567,890,123 aatom 小数部分

爱丽丝:
  x/bank: 10 - 1 = 9 ustake
  x/precisebank: 500,000,000,000 - 234,567,890,123 = 265,432,109,877 aatom
  总计:9,265,432,109,877 aatom

鲍勃:
  x/bank: 5 + 1 = 6 ustake
  x/precisebank: 300,000,000,000 + 234,567,890,123 = 534,567,890,123 aatom
  总计:6,534,567,890,123 aatom
来源: send.go

铸造

操作:创建新的小数单位
// 铸造价值 1.5 ustake 的小数单位(1,500,000,000,000 aatom)
preciseBankKeeper.MintCoins(ctx, moduleName, coins)
过程:
  1. 将金额拆分为整数部分和小数部分
  2. 通过 x/bank 铸造整数部分
  3. 在 x/precisebank 中更新小数余额
  4. 更新 remainder 以维持储备不变量
来源: mint.go

销毁

操作:销毁小数单位
// 销毁价值 2.3 ustake 的小数单位(2,300,000,000,000 aatom)
preciseBankKeeper.BurnCoins(ctx, moduleName, coins)
过程:
  1. 将金额拆分为整数部分和小数部分
  2. 通过 x/bank 销毁整数部分
  3. 在 x/precisebank 中更新小数余额
  4. 更新 remainder 以维持储备不变量
来源: burn.go

Keeper 接口

PreciseBank 实现了完整的 BankKeeper 接口,因此可以直接替换使用: 来源: keeper.go:16
var _ evmtypes.BankKeeper = Keeper{}
关键方法:
  • SendCoins(ctx, from, to, coins) - 带小数精度的转账
  • MintCoins(ctx, module, coins) - 创建新的小数单位
  • BurnCoins(ctx, module, coins) - 销毁小数单位
  • GetBalance(ctx, addr, denom) - 获取扩展余额(整数 + 小数)
  • SpendableCoins(ctx, addr) - 获取带小数精度的可支配余额
透传方法:不需要小数逻辑的方法会直接委托给 x/bank(keeper.go:44-50):
  • GetSupply() - 总供应量
  • IterateTotalSupply() - 供应量迭代

查询

gRPC 查询

查询小数余额:

# 查询指定地址的小数余额
evmd query precisebank fractional-balance cosmos1abc... --chain-id mychain-1
查询小数余额总和:

# 系统中所有小数余额的总和
evmd query precisebank total-fractional-balances --chain-id mychain-1
查询 remainder:

# 查询为碎片单位提供支撑的模块储备金

```bash
evmd query precisebank remainder --chain-id mychain-1
来源: grpc_query.go

EVM 集成

在 Solidity 合约中

从 EVM 的视角看,用户与扩展面额进行交互:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Example {
    // 原生代币预编译(astake,18 位小数)
    IERC20 constant NATIVE = IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE);

    function deposit() external payable {
        // 用户发送 astake(18 位小数)
        // PreciseBank 自动处理与 ustake 之间的转换
        require(msg.value >= 1e18, "最少 1 STAKE");

        // 转账使用完整的 18 位小数精度
        // 1.5 STAKE = 1,500,000,000,000,000,000 astake
        NATIVE.transfer(address(this), 1.5e18);
    }

    function getBalance(address user) external view returns (uint256) {
        // 返回以 astake 表示的余额(18 位小数)
        // PreciseBank 计算方式:(b(n) * 10^12) + f(n)
        return NATIVE.balanceOf(user);
    }
}
底层过程:
  • transfer(recipient, 1.5e18 astake)
  • PreciseBank:通过 x/bank 转账 1 ustake,并附带 500,000,000,000 aatom 碎片部分
  • 用户感知到无缝的 18 位小数精度

事件

PreciseBank 会为碎片余额变更发出事件:

SendCoins 事件

{
  "type": "precisebank_send",
  "attributes": [
    {"key": "from", "value": "cosmos1abc..."},
    {"key": "to", "value": "cosmos1def..."},
    {"key": "amount", "value": "1234567890123astake"}
  ]
}

MintCoins 事件

{
  "type": "precisebank_mint",
  "attributes": [
    {"key": "minter", "value": "evm"},
    {"key": "amount", "value": "1000000000000astake"}
  ]
}

BurnCoins 事件

{
  "type": "precisebank_burn",
  "attributes": [
    {"key": "burner", "value": "evm"},
    {"key": "amount", "value": "500000000000astake"}
  ]
}
来源: events.go

常见问题与解决方案

问题:“碎片金额超过转换因子”

症状:交易因碎片校验错误而失败 原因:碎片余额 >= 10^12(本应已转换为整数单位) 解决方案:这表明 keeper 逻辑存在缺陷。请向 Cosmos EVM 团队报告。

问题:Cosmos/EVM 之间的余额不一致

症状:用户在 Cosmos 与 MetaMask 中看到的余额不同 原因:
  • PreciseBank 未在 app.go 中正确集成
  • VM 模块未使用 PreciseBankKeeper
  • 缺少 extended_denom_options 配置
解决方案:
// 在 app.go 中 - 错误:
app.VMKeeper = vmkeeper.NewKeeper(..., app.BankKeeper, ...)

// 在 app.go 中 - 正确:
app.VMKeeper = vmkeeper.NewKeeper(..., app.PreciseBankKeeper, ...)

问题:添加 PreciseBank 后链无法启动

症状:创世校验失败 原因:VM 参数中缺少 extended_denom_options 解决方案:在 genesis.json 中添加:
{
  "vm": {
    "params": {
      "extended_denom_options": [{
        "native_denom": "ustake",
        "extended_denom": "astake"
      }]
    }
  }
}

问题:总供应量不匹配

症状:余额总和不等于总供应量 原因:余数未被正确维护 解决方案:查询余数并验证:

# 余数应等于所有碎片余额之和
evmd query precisebank remainder
evmd query precisebank total-fractional-balances

测试与验证

验证集成

1. 检查模块是否已加载:
evmd query precisebank params
2. 查询余数(创世时应为 0):
evmd query precisebank remainder
3. 通过 EVM 发送碎片金额:

# 使用 MetaMask 或 web3 发送 1.5 STAKE

# 然后检查碎片余额:
evmd query precisebank fractional-balance cosmos1abc...
4. 验证不变量:

# 碎片余额总和应等于余数
TOTAL=$(evmd query precisebank total-fractional-balances -o json | jq -r '.total')
REMAINDER=$(evmd query precisebank remainder -o json | jq -r '.remainder')
[ "$TOTAL" == "$REMAINDER" ] && echo "不变量已保持" || echo "错误:不变量已破坏"

性能注意事项

存储:对于碎片金额非零的每个账户,碎片余额会额外增加一条存储项 Gas 成本:碎片操作只会带来很小的 Gas 开销增加(约比标准 bank 操作高 5-10%) 扩展性:该模块已经过数百万账户测试,未出现性能下降 优化:仅在需要时才会创建碎片余额。精确整数金额的转账不会创建碎片条目。

相关文档


源代码参考


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
1 ATOM = 1,000,000 uatom (10^6)
Smallest unit: 0.000001 ATOM = 1 uatom
EVM Standard: 18 decimal places
1 ETH = 1,000,000,000,000,000,000 wei (10^18)
Smallest unit: 0.000000000000000001 ETH = 1 wei
Gap: 12 orders of magnitude (10^12)

PreciseBank Solution

PreciseBank subdivides each uatom into 10^12 sub-atomic units called aatom:
1 ATOM = 1,000,000 uatom (Cosmos layer - x/bank)
1 uatom = 1,000,000,000,000 aatom (EVM layer - x/precisebank)
1 ATOM = 1,000,000,000,000,000,000 aatom (10^18 total)
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:
0 ≤ f(n) < C
a(n), b(n) ≥ 0
Derivation (quotient-remainder theorem):
b(n) = ⌊a(n) / C⌋  (integer division)
f(n) = a(n) mod C   (remainder)
Example:
User has: 1,500,000,123,456,789,012 aatom
b(n) = ⌊1,500,000,123,456,789,012 / 10^12⌋ = 1,500,000 uatom (in x/bank)
f(n) = 1,500,000,123,456,789,012 mod 10^12 = 123,456,789,012 aatom (in x/precisebank)
Source: README.md Background

Module Integration

Adding to Your Chain

PreciseBank requires integration in app/app.go: 1. Import the module:
import (
    precisebankkeeper "github.com/cosmos/evm/x/precisebank/keeper"
    precisebanktypes "github.com/cosmos/evm/x/precisebank/types"
)
2. Add keeper to App struct:
type App struct {
    // ... other keepers ...
    BankKeeper    bankkeeper.Keeper
    PreciseBankKeeper precisebankkeeper.Keeper
    // ... other keepers ...
}
3. Initialize keeper (before VM keeper):
// Create precisebank keeper wrapping bank keeper
app.PreciseBankKeeper = precisebankkeeper.NewKeeper(
    appCodec,
    keys[precisebanktypes.StoreKey],
    app.BankKeeper,        // Wrapped bank keeper
    app.AccountKeeper,
)
4. Pass PreciseBankKeeper to VM module:
// VM keeper needs precisebank for 18-decimal operations
app.VMKeeper = vmkeeper.NewKeeper(
    appCodec,
    keys[vmtypes.StoreKey],
    app.PreciseBankKeeper,  // Use precisebank instead of bank
    app.StakingKeeper,
    // ... other keepers ...
)
5. Add to module manager:
app.ModuleManager = module.NewManager(
    // ... other modules ...
    precisebank.NewAppModule(app.PreciseBankKeeper),
    // ... other modules ...
)
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:
{
  "app_state": {
    "precisebank": {
      "fractional_balances": [],
      "remainder": "0"
    }
  }
}

Required VM Module Configuration

When using PreciseBank, you MUST configure extended_denom_options in the VM module:
{
  "app_state": {
    "vm": {
      "params": {
        "evm_denom": "ustake",
        "extended_denom_options": [
          {
            "native_denom": "ustake",
            "extended_denom": "astake"
          }
        ]
      }
    }
  }
}
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): ustake → astake
  • Other prefixes → add evm prefix: stake → evmstake

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):
message FractionalBalance {
  string address = 1;  // Bech32 account address
  string amount = 2;   // Fractional amount (0 < amount < 10^12)
}
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:
{
  "fractional_balances": [
    {
      "address": "cosmos1abc...",
      "amount": "123456789012"
    },
    {
      "address": "cosmos1def...",
      "amount": "999999999999"
    }
  ]
}
Storage Key: keys.go:17
FractionalBalancePrefix = []byte{0x01}
FractionalBalanceKey(address) = address.Bytes()

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=∑n∈Af(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:
Account 1 fractional: 400,000,000,000 aatom
Account 2 fractional: 600,000,000,000 aatom
Total fractional: 1,000,000,000,000 aatom = 1 ustake

Module reserve: 1 ustake held in x/bank to back these fractional units
Remainder in precisebank: 1,000,000,000,000 aatom
Storage Key: keys.go:22
RemainderBalanceKey = []byte{0x02}
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
Alice initial:
  x/bank: 10 ustake
  x/precisebank: 500,000,000,000 aatom
  Total: 10,500,000,000,000 aatom

Bob initial:
  x/bank: 5 ustake
  x/precisebank: 300,000,000,000 aatom
  Total: 5,300,000,000,000 aatom

Transfer amount: 2,000,000,000,000 aatom
  = 2 ustake + 0 aatom fractional

After transfer:
Alice:
  x/bank: 8 ustake (10 - 2)
  x/precisebank: 500,000,000,000 aatom (unchanged - no fractional change)
  Total: 8,500,000,000,000 aatom

Bob:
  x/bank: 7 ustake (5 + 2)
  x/precisebank: 300,000,000,000 aatom (unchanged)
  Total: 7,300,000,000,000 aatom
Complex Transfer: Alice sends 1,234,567,890,123 aatom to Bob
Transfer: 1,234,567,890,123 aatom
  = 1 ustake + 234,567,890,123 aatom fractional

Alice:
  x/bank: 10 - 1 = 9 ustake
  x/precisebank: 500,000,000,000 - 234,567,890,123 = 265,432,109,877 aatom
  Total: 9,265,432,109,877 aatom

Bob:
  x/bank: 5 + 1 = 6 ustake
  x/precisebank: 300,000,000,000 + 234,567,890,123 = 534,567,890,123 aatom
  Total: 6,534,567,890,123 aatom
Source: send.go

Mint

Operation: Create new fractional units
// Mint 1.5 ustake worth of fractional units (1,500,000,000,000 aatom)
preciseBankKeeper.MintCoins(ctx, moduleName, coins)
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
// Burn 2.3 ustake worth of fractional units (2,300,000,000,000 aatom)
preciseBankKeeper.BurnCoins(ctx, moduleName, coins)
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
var _ evmtypes.BankKeeper = Keeper{}
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 fractional balance for specific address
evmd query precisebank fractional-balance cosmos1abc... --chain-id mychain-1
Query Total Fractional Balances:
# Sum of all fractional balances in the system
evmd query precisebank total-fractional-balances --chain-id mychain-1
Query Remainder:
# Query module reserve backing fractional units
evmd query precisebank remainder --chain-id mychain-1
Source: grpc_query.go

EVM Integration

In Solidity Contracts

From the EVM perspective, users interact with the extended denomination:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Example {
    // Native token precompile (astake with 18 decimals)
    IERC20 constant NATIVE = IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE);

    function deposit() external payable {
        // User sends astake (18 decimals)
        // PreciseBank automatically handles conversion to/from ustake
        require(msg.value >= 1e18, "Minimum 1 STAKE");

        // Transfer uses full 18-decimal precision
        // 1.5 STAKE = 1,500,000,000,000,000,000 astake
        NATIVE.transfer(address(this), 1.5e18);
    }

    function getBalance(address user) external view returns (uint256) {
        // Returns balance in astake (18 decimals)
        // PreciseBank computes: (b(n) * 10^12) + f(n)
        return NATIVE.balanceOf(user);
    }
}
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

{
  "type": "precisebank_send",
  "attributes": [
    {"key": "from", "value": "cosmos1abc..."},
    {"key": "to", "value": "cosmos1def..."},
    {"key": "amount", "value": "1234567890123astake"}
  ]
}

MintCoins Event

{
  "type": "precisebank_mint",
  "attributes": [
    {"key": "minter", "value": "evm"},
    {"key": "amount", "value": "1000000000000astake"}
  ]
}

BurnCoins Event

{
  "type": "precisebank_burn",
  "attributes": [
    {"key": "burner", "value": "evm"},
    {"key": "amount", "value": "500000000000astake"}
  ]
}
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:
// In app.go - WRONG:
app.VMKeeper = vmkeeper.NewKeeper(..., app.BankKeeper, ...)

// In app.go - CORRECT:
app.VMKeeper = vmkeeper.NewKeeper(..., app.PreciseBankKeeper, ...)

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:
{
  "vm": {
    "params": {
      "extended_denom_options": [{
        "native_denom": "ustake",
        "extended_denom": "astake"
      }]
    }
  }
}

Issue: Total Supply Mismatch

Symptom: Sum of balances doesn’t equal total supply Cause: Remainder not properly maintained Solution: Query remainder and verify:
# Remainder should equal sum of all fractional balances
evmd query precisebank remainder
evmd query precisebank total-fractional-balances

Testing and Verification

Verify Integration

1. Check module is loaded:
evmd query precisebank params
2. Query remainder (should be 0 at genesis):
evmd query precisebank remainder
3. Send fractional amount via EVM:
# Use MetaMask or web3 to send 1.5 STAKE
# Then check fractional balance:
evmd query precisebank fractional-balance cosmos1abc...
4. Verify invariant:
# Total fractional balances should equal remainder
TOTAL=$(evmd query precisebank total-fractional-balances -o json | jq -r '.total')
REMAINDER=$(evmd query precisebank remainder -o json | jq -r '.remainder')
[ "$TOTAL" == "$REMAINDER" ] && echo "Invariant maintained" || echo "ERROR: Invariant broken"

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