x/precisebank)将标准 Cosmos SDK bank 模块的精度从 6 位小数扩展到 18 位小数,在保持 Cosmos coin 完整性的同时实现完整的 EVM 兼容性。对于使用非 18 位小数原生代币的链,此模块是必需的。
特别感谢 Kava 团队为该模块做出的宝贵贡献。
模块概览
用途:弥合 Cosmos(通常为 6 位小数)与 EVM(18 位小数)之间的小数精度差距 核心功能:- 在不改变基础面额的情况下扩展代币精度
- 将小数余额(亚原子单位)与整数余额分开跟踪
- 保持小数单位与整数单位之间 1:1 的储备支撑
- 对用户透明,余额在两个环境中的显示都符合预期
- 为
x/vm包装x/bank,提供 18 位小数精度
何时需要 PreciseBank?
如果满足以下情况,你需要 PreciseBank:
你的原生代币有 6 位小数(或任何非 18 位小数位数):- 基础面额:
ustake、utoken、uatom(micro 前缀 = 10^6) - 展示面额:
stake、token、atom - 示例:1 STAKE = 1,000,000 ustake = 10^6 个最小单位
如果满足以下情况,你不需要 PreciseBank:
你的原生代币有 18 位小数:- 基础面额:
atest、atoken(atto 前缀 = 10^18) - 展示面额:
test、token - 示例:1 TEST = 1,000,000,000,000,000,000 atest = 10^18 个最小单位
数学基础
精度问题
Cosmos 标准:6 位小数PreciseBank 方案
PreciseBank 将每个uatom 细分为 10^12 个称为 aatom 的亚原子单位:
aatom 都由 x/bank 中的 uatom 完整支撑。没有对应的整数 uatom 储备,就不能存在小数 aatom。
余额表示
对于任意账户n,其以亚原子单位表示的总余额 a(n) 为:
其中:
a(n)=aatom总余额(18 位小数表示)b(n)= 整数uatom余额(存储在 x/bank 中)f(n)= 小数余额(存储在 x/precisebank 中)C= 转换因子 = 10^12
模块集成
添加到你的链中
PreciseBank 需要集成到app/app.go 中:
1. 导入模块:
配置
Genesis 配置
PreciseBank 的 genesis 配置非常少,它主要跟踪状态而不是参数。 文件位置:~/.evmd/config/genesis.json 中的 app_state.precisebank
结构:
必需的 VM 模块配置
使用 PreciseBank 时,你必须在 VM 模块中配置extended_denom_options:
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):
- 金额必须为正(
amount > 0) - 金额必须小于转换因子(
amount < 10^12) - 地址必须是有效的 Bech32
remainder
存储内容:模块级储备余额,为流通中的所有小数单位提供支撑。 类型:整数(sdkmath.Int) 作用:维持“所有小数余额之和等于模块储备”的不变量 不变量: 其中:remainder= 以小数单位表示的模块储备- = 所有账户小数余额之和
操作
转账
在转移小数金额时,PreciseBank 会自动处理其中的复杂性: 示例转账:爱丽丝向鲍勃发送 1.5 ustake + 5000 亿 aatom铸造
操作:创建新的小数单位- 将金额拆分为整数部分和小数部分
- 通过 x/bank 铸造整数部分
- 在 x/precisebank 中更新小数余额
- 更新 remainder 以维持储备不变量
销毁
操作:销毁小数单位- 将金额拆分为整数部分和小数部分
- 通过 x/bank 销毁整数部分
- 在 x/precisebank 中更新小数余额
- 更新 remainder 以维持储备不变量
Keeper 接口
PreciseBank 实现了完整的BankKeeper 接口,因此可以直接替换使用:
来源: keeper.go:16
SendCoins(ctx, from, to, coins)- 带小数精度的转账MintCoins(ctx, module, coins)- 创建新的小数单位BurnCoins(ctx, module, coins)- 销毁小数单位GetBalance(ctx, addr, denom)- 获取扩展余额(整数 + 小数)SpendableCoins(ctx, addr)- 获取带小数精度的可支配余额
GetSupply()- 总供应量IterateTotalSupply()- 供应量迭代
查询
gRPC 查询
查询小数余额:EVM 集成
在 Solidity 合约中
从 EVM 的视角看,用户与扩展面额进行交互:transfer(recipient, 1.5e18 astake)- PreciseBank:通过 x/bank 转账 1 ustake,并附带 500,000,000,000 aatom 碎片部分
- 用户感知到无缝的 18 位小数精度
事件
PreciseBank 会为碎片余额变更发出事件:SendCoins 事件
MintCoins 事件
BurnCoins 事件
常见问题与解决方案
问题:“碎片金额超过转换因子”
症状:交易因碎片校验错误而失败 原因:碎片余额 >= 10^12(本应已转换为整数单位) 解决方案:这表明 keeper 逻辑存在缺陷。请向 Cosmos EVM 团队报告。问题:Cosmos/EVM 之间的余额不一致
症状:用户在 Cosmos 与 MetaMask 中看到的余额不同 原因:- PreciseBank 未在 app.go 中正确集成
- VM 模块未使用 PreciseBankKeeper
- 缺少
extended_denom_options配置
问题:添加 PreciseBank 后链无法启动
症状:创世校验失败 原因:VM 参数中缺少extended_denom_options
解决方案:在 genesis.json 中添加:
问题:总供应量不匹配
症状:余额总和不等于总供应量 原因:余数未被正确维护 解决方案:查询余数并验证:测试与验证
验证集成
1. 检查模块是否已加载:性能注意事项
存储:对于碎片金额非零的每个账户,碎片余额会额外增加一条存储项 Gas 成本:碎片操作只会带来很小的 Gas 开销增加(约比标准 bank 操作高 5-10%) 扩展性:该模块已经过数百万账户测试,未出现性能下降 优化:仅在需要时才会创建碎片余额。精确整数金额的转账不会创建碎片条目。相关文档
源代码参考
- 模块实现: x/precisebank
- README(数学背景): x/precisebank/README.md
- Keeper: keeper/keeper.go
- 碎片余额逻辑: types/fractional_balance.go
- 发送操作: keeper/send.go
- 铸造操作: keeper/mint.go
- 销毁操作: keeper/burn.go
- 余数管理: keeper/remainder_amount.go
- 存储键: types/keys.go
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/bankto provide 18-decimal precision forx/vm
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
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
Mathematical Foundation
The Precision Problem
Cosmos Standard: 6 decimal placesPreciseBank Solution
PreciseBank subdivides eachuatom into 10^12 sub-atomic units called aatom:
aatom is fully backed by uatom in x/bank. You cannot have fractional aatom without corresponding integer uatom reserves.
Balance Representation
For any accountn, the total balance in sub-atomic units a(n) is:
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
Module Integration
Adding to Your Chain
PreciseBank requires integration inapp/app.go:
1. Import the module:
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 configureextended_denom_options in the VM module:
native_denom: 6-decimal Cosmos denom (ustake)extended_denom: 18-decimal EVM denom (astake)- Conversion: 1 ustake = 10^12 astake
uprefix (micro, 10^6) →aprefix (atto, 10^18):ustake→astake- Other prefixes → add
evmprefix: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 ofFractionalBalance objects
Structure (fractional_balance.go:43-48):
- Amount must be positive (
amount > 0) - Amount must be less than conversion factor (
amount < 10^12) - Address must be valid Bech32
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: Where:remainder= Module reserve in fractional units- = Sum of all account fractional balances
Operations
Transfer
When transferring fractional amounts, PreciseBank handles the complexity automatically: Example Transfer: Alice sends 1.5 ustake + 500 billion aatom to BobMint
Operation: Create new fractional units- Split amount into integer and fractional parts
- Mint integer part via x/bank
- Update fractional balance in x/precisebank
- Update remainder to maintain backing invariant
Burn
Operation: Destroy fractional units- Split amount into integer and fractional parts
- Burn integer part via x/bank
- Update fractional balance in x/precisebank
- Update remainder to maintain backing invariant
Keeper Interface
PreciseBank implements the fullBankKeeper interface, making it a drop-in replacement:
Source: keeper.go:16
SendCoins(ctx, from, to, coins)- Transfer with fractional precisionMintCoins(ctx, module, coins)- Create new fractional unitsBurnCoins(ctx, module, coins)- Destroy fractional unitsGetBalance(ctx, addr, denom)- Get extended balance (integer + fractional)SpendableCoins(ctx, addr)- Get spendable balances with fractional precision
GetSupply()- Total supplyIterateTotalSupply()- Supply iteration
Queries
gRPC Queries
Query Fractional Balance:EVM Integration
In Solidity Contracts
From the EVM perspective, users interact with the extended denomination: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
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_optionsconfiguration
Issue: Chain Won’t Start After Adding PreciseBank
Symptom: Genesis validation fails Cause: Missingextended_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: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.Related Documentation
- Building Your Chain Guide - Main configuration walkthrough
- VM Module - extended_denom_options setup
- ERC20 Module - Token pair configuration
Source Code References
- Module Implementation: x/precisebank
- README (Math Background): x/precisebank/README.md
- Keeper: keeper/keeper.go
- Fractional Balance Logic: types/fractional_balance.go
- Send Operations: keeper/send.go
- Mint Operations: keeper/mint.go
- Burn Operations: keeper/burn.go
- Remainder Management: keeper/remainder_amount.go
- Storage Keys: types/keys.go