XCN Decimal Handling

Critical information about XCN token decimal handling differences from Ethereum

⚠️ Critical: 8 vs 18 Decimals Discrepancy

Overview

On Goliath, the native XCN token has a unique decimal system that differs significantly from Ethereum's standard 18 decimals everywhere approach. This discrepancy exists between the internal EVM representation and the external JSON-RPC interface.

Quick Reference

Context
Decimals
1 XCN equals
Used in

EVM Internal

8

10^8 tinyxcns

msg.value, address.balance, Solidity

JSON-RPC

18

10^18 weixcns

eth_getBalance, MetaMask, web3.js

Conversion

-

1 tinyxcn = 10^10 weixcns

Between systems

Understanding the Two Systems

EVM Internal (8 decimals)

Inside smart contracts, all native token operations use tinyxcns with 8 decimal places:

// 1 XCN = 100,000,000 tinyxcns = 10^8 tinyxcns
uint256 constant ONE_XCN = 1e8;

This affects:

  • msg.value in payable functions

  • address.balance queries

  • transfer() and call{value: }() operations

  • All arithmetic with native token amounts

JSON-RPC Interface (18 decimals)

External tools and libraries see balances in weixcns with 18 decimal places:

// 1 XCN = 1,000,000,000,000,000,000 weixcns = 10^18 weixcns
const oneXCN = ethers.parseEther("1"); // Returns 1e18

This affects:

  • eth_getBalance RPC calls

  • MetaMask balance displays

  • Ethers.js/Web3.js balance queries

  • Foundry's cast balance command

Conversion Formula

Balance_RPC = Balance_EVM × 10^10

Where:
- Balance_RPC is in weixcns (18 decimals)
- Balance_EVM is in tinyxcns (8 decimals)

Practical Example

Consider an address with 9,000,099.81460509 XCN:

Inside Smart Contract

// Solidity view
uint256 balance = address(this).balance;
// Returns: 900,009,981,460,509 (tinyxcns)
// This is 9,000,099.81460509 × 10^8

Via JSON-RPC

// JavaScript view
const balance = await provider.getBalance(address);
// Returns: "9000099814605090000000000" (weixcns)
// This is 9,000,099.81460509 × 10^18

Verification

9,000,099,814,605,090,000,000,000 ÷ 900,009,981,460,509 = 10,000,000,000 = 10^10 ✓

Smart Contract Development

❌ Common Mistakes

// ❌ WRONG - This is 10 billion XCN!
uint256 public constant ONE_XCN = 1e18;

// ❌ WRONG - Using Ethereum's ether keyword
require(msg.value >= 1 ether, "Send 1 XCN");

Best Practices

1. Define Clear Constants

contract XCNPayment {
    uint256 constant XCN_DECIMALS = 8;
    uint256 constant ONE_XCN = 10 ** XCN_DECIMALS;      // 1e8
    uint256 constant HALF_XCN = ONE_XCN / 2;            // 5e7
    uint256 constant MIN_PAYMENT = ONE_XCN / 100;       // 0.01 XCN

    // For display/logging purposes
    uint256 constant DISPLAY_DECIMALS = 2;
}

2. Handle Payments Correctly

function deposit() external payable {
    // msg.value is in tinyxcns (8 decimals)
    require(msg.value >= ONE_XCN, "Minimum 1 XCN required");

    // Calculate amount in XCN for events/display
    uint256 amountInXCN = msg.value / ONE_XCN;
    uint256 remainder = msg.value % ONE_XCN;

    emit Deposit(msg.sender, amountInXCN, remainder);
}

3. Balance Management

function getBalanceInXCN(address account) public view returns (
    uint256 whole,
    uint256 fractional
) {
    uint256 balanceTinybars = account.balance;
    whole = balanceTinybars / ONE_XCN;
    fractional = balanceTinybars % ONE_XCN;
}

function withdraw(uint256 amountXCN) external {
    uint256 amountTinybars = amountXCN * ONE_XCN;
    require(address(this).balance >= amountTinybars, "Insufficient balance");

    (bool success, ) = msg.sender.call{value: amountTinybars}("");
    require(success, "Transfer failed");
}

Frontend Integration

Sending Transactions

// User wants to send 1.5 XCN
const amountInXCN = 1.5;

// Convert to weixcns for RPC (18 decimals)
const amountInWeibars = ethers.parseEther(amountInXCN.toString());
// Sends: 1,500,000,000,000,000,000 weixcns

// The EVM receives this as tinyxcns (8 decimals)
// EVM sees: 150,000,000 tinyxcns (correct: 1.5 × 10^8)

await contract.deposit({ value: amountInWeibars });

Reading Balances

// Get balance via RPC (returns 18 decimals)
const balanceWei = await provider.getBalance(address);
// Example: "9000099814605090000000000"

// Convert to human-readable XCN
const balanceXCN = ethers.formatEther(balanceWei);
// Result: "9000099.81460509"

// If you need tinyxcns (to match contract's view)
const balanceTinybars = balanceWei / 10n**10n;
// Result: 900009981460509n

Utility Functions

// Conversion utilities for your dApp
const XCNUtils = {
    // Convert between representations
    weiToTinybars: (wei) => {
        return BigInt(wei) / 10_000_000_000n; // ÷ 10^10
    },

    tinyxcnsToWei: (tinyxcns) => {
        return BigInt(tinyxcns) * 10_000_000_000n; // × 10^10
    },

    tinyxcnsToXCN: (tinyxcns) => {
        return Number(tinyxcns) / 1e8;
    },

    weiToXCN: (wei) => {
        return Number(wei) / 1e18;
    },

    // For user input
    parseXCN: (xcnString) => {
        return ethers.parseEther(xcnString); // Returns weixcns
    },

    // For display
    formatXCN: (weixcns) => {
        return ethers.formatEther(weixcns); // Returns XCN string
    }
};

Testing Considerations

Hardhat Configuration

// hardhat.config.js
module.exports = {
    networks: {
        goliathTestnet: {
            url: "https://testnet-rpc.goliath.net",
            accounts: [process.env.PRIVATE_KEY],
            chainId: 8901,
            // Note: Gas prices are in weixcns (18 decimals)
        }
    }
};

Test Examples

describe("XCN Payment Contract", function() {
    it("Should handle 1 XCN deposit correctly", async function() {
        // Send 1 XCN using ethers (18 decimals for RPC)
        await contract.deposit({
            value: ethers.parseEther("1") // 1e18 weixcns
        });

        // Contract sees this as 1e8 tinyxcns
        const contractBalance = await contract.getBalance();
        expect(contractBalance).to.equal(100_000_000); // tinyxcns

        // RPC returns 18 decimals
        const rpcBalance = await ethers.provider.getBalance(contract.address);
        expect(rpcBalance).to.equal(ethers.parseEther("1")); // 1e18 weixcns
    });

    it("Should calculate fees correctly", async function() {
        const depositAmount = ethers.parseEther("100"); // 100 XCN

        // Contract takes 1% fee in tinyxcns
        await contract.depositWithFee({ value: depositAmount });

        // Check fee calculation (contract works in tinyxcns)
        const fee = await contract.getFeeForAmount(10_000_000_000); // 100 XCN in tinyxcns
        expect(fee).to.equal(100_000_000); // 1 XCN fee (1% of 100)
    });
});

Common Pitfalls

1. Porting Ethereum Contracts

// Ethereum contract (DON'T copy directly)
contract EthereumVault {
    uint256 public constant MIN_DEPOSIT = 0.1 ether; // 1e17 wei

    function deposit() external payable {
        require(msg.value >= MIN_DEPOSIT, "Too small");
    }
}

// Goliath adaptation (DO this instead)
contract GoliathVault {
    uint256 public constant MIN_DEPOSIT = 1e7; // 0.1 XCN = 1e7 tinyxcns

    function deposit() external payable {
        require(msg.value >= MIN_DEPOSIT, "Too small");
    }
}

2. Event Emissions

// Be clear about units in events
event Deposit(
    address indexed user,
    uint256 amountTinybars,  // Raw msg.value
    uint256 amountXCN        // Human-readable whole XCN
);

function deposit() external payable {
    emit Deposit(
        msg.sender,
        msg.value,           // tinyxcns (8 decimals)
        msg.value / ONE_XCN  // whole XCN units
    );
}

3. Price Calculations

// Price feeds and calculations
contract PriceCalculator {
    uint256 constant XCN_DECIMALS = 8;
    uint256 constant USD_DECIMALS = 8;  // Example: Chainlink price feeds

    function calculateXCNAmount(
        uint256 usdAmount,
        uint256 xcnPriceInUSD  // Price with USD_DECIMALS
    ) public pure returns (uint256) {
        // Convert USD to XCN tinyxcns
        return (usdAmount * 10**XCN_DECIMALS) / xcnPriceInUSD;
    }
}

Migration Checklist

When migrating from Ethereum:

Summary

The decimal difference between EVM internal (8) and JSON-RPC (18) is a fundamental characteristic of Goliath's XCN token. Understanding and properly handling this difference is crucial for building secure and functional applications on Goliath.

Last updated