Skip to content

https://github.com/crypto-org-chain/cronos/releases ICS-721 x/nft-transfer: Partial Mint → Double-Spending / State Inconsistency #2002

@ea9436946-hub

Description

@ea9436946-hub

Describe the bug

A critical Double-Spending / Infinite Mint vulnerability exists in the x/nft-transfer module (ICS-721) of the Cronos POS Chain. The IBC packet processing is non-atomic due to a missing sdk.CacheContext in the packet handler. During a multi-NFT batch transfer, partial failures (e.g., deterministic gas exhaustion) cause some NFTs to be permanently minted on the destination chain while the source chain refunds all original assets, effectively cloning NFTs.

Severity: Extreme (Mainnet protocol-level impact)

Affected Asset: Cronos POS Chain (Mainnet Core)
Repository Reference: Cronos chain-main GitHub

To Reproduce
Steps to reproduce the behavior:

  1. Select a high-value NFT collection supported by the Cronos IBC bridge.
  2. Construct an IBC NonFungibleTokenPacketData containing a batch of 50 Token IDs (replace with actual IDs if available).
  3. Profile the deterministic gas cost for a single MintNFT (~10k–15k gas per iteration).
  4. Send an IBC transaction with a Gas Limit set to fail mid-loop (e.g., at the 25th NFT).
  5. Observe:
    Destination chain: first NFTs are permanently minted
    Source chain: refunds all original NFTs
    Result: Duplicate NFTs exist → Double-Spending / Infinite Min

Expected behavior
The IBC transaction should be atomic: either all NFTs are minted on the destination chain, or none. Partial commits must never occur.

Screenshots / PoC
.Attach evidence showing:
.artial NFT minting
.Source chain refund
.Duplicate NFTs on Cronos
.Optional: attach exploit_poc.go or TX Hashes demonstrating the issue

Desktop (please complete the following information):
OS: Kali Linux / Ubuntu
Browser: Chrome / Firefox
Version: latest

Smartphone (optional):
Device / OS / Browser: N/A (if applicable)
Additional context

Additional context Vulnerable Code:

// x/nft-transfer/ibc_module.go:169
if err := im.keeper.OnRecvPacket(ctx, channelVersion, packet, data); err != nil {
ack = types.NewErrorAcknowledgement(err)
// ERROR CAUGHT BUT NO ROLLBACK PERFORMED
}

// x/nft-transfer/keeper/packet.go:180-184
for i, tokenID := range data.TokenIds {
// Each successful call here is PERMANENTLY COMMITTED to the KVStore
if err := k.nftKeeper.MintNFT(ctx, voucherClassID, tokenID, ..., receiver); err != nil {
return err // Partial failure aborts loop, but previous changes persist
}
}

Proof of Concept:
.Shows state inconsistency: NFTs minted on Cronos while refunded on source chain
.TX Hashes / Token IDs can be added for verification
.Screenshots / demonstrate duplication

Recommended Fix:

cacheCtx, writeCache := ctx.CacheContext()
err := im.keeper.OnRecvPacket(cacheCtx, packet, data)
if err == nil {
writeCache() // Only commit if the entire batch succeeds
}
Next Steps for Devs:

Next Steps for Devs:

  • Validate PoC on Testnet / Localnet
  • Apply atomic execution fix with CacheContext
  • Ensure IBC packets either fully succeed or fully rollback
Image Image Image Image Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions