From 5967c8f13bbce87d1f3bc1f202312f10cc5d8181 Mon Sep 17 00:00:00 2001 From: Keith Kowal Date: Wed, 29 Jul 2026 14:11:17 -0700 Subject: [PATCH 1/2] docs(evm): clarify HBAR native value is tinybar inside the EVM (8 vs 18 decimals) The decimals table implied `msg.value` is 18-decimal wei, contradicting the row that (correctly) states the Smart Contract Service uses 8 decimals. A developer reading "JSON-RPC Relay (msg.value): 18 decimals" assumes wei-scale and gets a 10^10 error: a WETH-style deposit records balances 10^10 too small, and a contract-to-contract `call{value: 1 ether}` sends 1e18 *as tinybar* (over-/under-transfer). - hbar-decimals.mdx: mark the 18-decimal row as an RPC-boundary convention, note the relay converts weibar->tinybar before execution, and add a warning with worked examples (relay path under-count; contract-to-contract call). - native-token-transfers.mdx: state that `value`/`msg.value` is tinybar during execution; warn against `1 ether`/wei-scale amounts; annotate the example. Signed-off-by: Keith Kowal --- evm/differences/hbar-decimals.mdx | 19 +++++++++++++++++-- evm/differences/native-token-transfers.mdx | 9 +++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/evm/differences/hbar-decimals.mdx b/evm/differences/hbar-decimals.mdx index a0415230..95ea78f7 100644 --- a/evm/differences/hbar-decimals.mdx +++ b/evm/differences/hbar-decimals.mdx @@ -13,13 +13,28 @@ Managing token decimals is critical when working with HBAR, HTS tokens, and ERC The table below compares the decimal handling of HBAR, HTS tokens, and ERC tokens on Hedera, incorporating details about their representation across APIs and services. This overview highlights differences in precision and context. -
API/ServiceDecimalsExplanation
Hedera API (HAPI)8 decimalsHBAR is represented with 8 decimal places, aligning with its native smallest unit tinybar.
Hedera Smart Contract Service8 decimalsWithin the EVM environment, HBAR maintains 8 decimal places, consistent with its native representation.
JSON-RPC Relay (Arguments)8 decimalsWhen HBAR values are passed as arguments in JSON-RPC calls, they are represented with 8 decimal places.
JSON-RPC Relay (msg.value)18 decimalsFor compatibility with EVM tooling, msg.value in JSON-RPC Relay represents HBAR with 18 decimal places. Consequently, gasPrice also uses 18 decimal places in this context.
HTS TokensConfigurable (up to 8 decimals)HTS tokens allow token creators to define precision at token creation, offering flexibility for various use cases.
ERC TokensDefault 18 decimalsERC tokens on Hedera follow Ethereum token standards, with 18 decimals as the default unless specified otherwise.
+
API/ServiceDecimalsExplanation
Hedera API (HAPI)8 decimalsHBAR is represented with 8 decimal places, aligning with its native smallest unit tinybar.
Hedera Smart Contract Service (EVM execution)8 decimalsWithin the EVM, HBAR is tinybar-scaled: msg.value, address(this).balance, and the value passed to call/send/transfer are all 8 decimals during execution.
JSON-RPC Relay (Arguments)8 decimalsWhen HBAR values are passed as arguments in JSON-RPC calls, they are represented with 8 decimal places.
JSON-RPC Relay (msg.value / gasPrice) — RPC boundary only18 decimalsEthereum tooling submits and reads the transaction value and gasPrice in 18-decimal weibar at the JSON-RPC boundary. The relay converts weibar to tinybar (÷1010) before the EVM executes, so the msg.value your contract actually sees is 8-decimal tinybar — not 18-decimal wei. See the warning below.
HTS TokensConfigurable (up to 8 decimals)HTS tokens allow token creators to define precision at token creation, offering flexibility for various use cases.
ERC TokensDefault 18 decimalsERC tokens on Hedera follow Ethereum token standards, with 18 decimals as the default unless specified otherwise.
**Key Impacts**: * Account for scaling differences when converting HBAR between APIs, especially when using JSON-RPC. * HBAR fees are always calculated in tinybars, regardless of the API or service used. -* JSON-RPC’s use of 18 decimals ensures smooth integration with EVM tools and libraries. +* The 18-decimal representation is a **JSON-RPC boundary convention only** — inside the EVM, native value is tinybar (8 decimals). Do not assume `msg.value` is wei-scale (see warning). + + +**Inside a contract, native value is tinybar (8 decimals), not wei (18).** `msg.value`, `address(this).balance`, and the `value` you pass to `call`/`send`/`transfer` are all tinybar-denominated during EVM execution. Two cases to design for: + +- **Transactions from tooling (relay path).** When you send 1 HBAR as `value: 1 ether` (1e18 weibar), the relay divides by 1010, so your contract sees `msg.value == 1e8`. A contract that stores `msg.value` — e.g. a WETH-style wrapper reporting `decimals() = 18` — will record a balance 1010 smaller than an Ethereum developer would expect. +- **Contract-to-contract calls (no conversion).** `payable(x).call{value: 1 ether}("")` compiles `1 ether` to `1e18` and sends it **as tinybar** (= 1010 HBAR), which either reverts (insufficient balance) or over-transfers. Specify the value in tinybar instead. + +```solidity +// ❌ BROKEN — `1 ether` compiles to 1e18 and is treated as tinybar (= 10^10 HBAR) +(bool ok, ) = payable(vault).call{value: 1 ether}(""); + +// ✅ CORRECT — specify the value in tinybar (1 HBAR = 100,000,000 tinybar) +(bool ok, ) = payable(vault).call{value: 100000000}(""); +``` + *** diff --git a/evm/differences/native-token-transfers.mdx b/evm/differences/native-token-transfers.mdx index 90e4afdc..88a5b6ec 100644 --- a/evm/differences/native-token-transfers.mdx +++ b/evm/differences/native-token-transfers.mdx @@ -7,7 +7,7 @@ title: "Handling HBAR Transfers in Contracts" On Ethereum, sending ETH to a contract address automatically triggers the `receive()` or `fallback()` functions, allowing contracts to process incoming funds. On Hedera, these functions also exist but require HBAR to be explicitly sent via `contractCall` for them to execute. Direct HBAR transfers to a contract’s Hedera account won’t trigger any logic unless additional steps are taken. -Fortunately, the core Solidity patterns—like using `transfer()`, `send()`, or `call()`—work the same way on Hedera, making it easy for developers familiar with EVM. This guide highlights these mechanisms, details the key Hedera-specific considerations, and provides examples to help you handle HBAR transfers in your smart contracts +The core Solidity patterns—`transfer()`, `send()`, and `call()`—are supported on Hedera, with **one important difference: the `value` (and `msg.value`) is denominated in tinybar (8 decimals) during EVM execution, not wei (18 decimals).** This guide highlights these mechanisms, details the key Hedera-specific considerations, and provides examples to help you handle HBAR transfers in your smart contracts. ### **Sending to Contract** @@ -24,6 +24,10 @@ These methods are supported on Hedera, ensuring compatibility with existing Soli * **Fallback and Receive Functions**: When sending HBAR to a contract address via `contractCall`, Hedera behaves like Ethereum. If `receive()` or `fallback()` functions are defined in the contract, they will be triggered upon receipt of HBAR. * **Important Note**: Directly transferring HBAR to a contract’s Hedera account (not via `contractCall`) will not trigger these functions. To execute logic upon receipt, ensure transfers occur within the EVM environment. + +**The `value` in `transfer`/`send`/`call` is tinybar (8 decimals), not wei (18).** In the examples below, `_amount` is a tinybar amount — `1 HBAR = 100_000_000`. Passing an Ethereum-style `1 ether` or a wei-scale number sends that number **as tinybar**, over- or under-transferring by 1010 (it will revert on insufficient balance, or drain far more than intended). Use tinybar literals, or convert with the helpers in [Decimal Handling](/evm/differences/hbar-decimals). + + *** ## Example Contract Functions for HBAR Transfers @@ -37,7 +41,8 @@ receive() external payable { emit HbarReceived(msg.sender, msg.value); } -// Transfer HBAR using different methods +// Transfer HBAR using different methods. +// NOTE: `_amount` is in TINYBAR (8 decimals), not wei — 1 HBAR = 100_000_000. function transferHbar(address payable _receiverAddress, uint _amount) public { _receiverAddress.transfer(_amount); } From c3a097e3b5a062a8cef1ba9dea93841fb80338e3 Mon Sep 17 00:00:00 2001 From: Keith Kowal Date: Thu, 30 Jul 2026 08:15:51 -0700 Subject: [PATCH 2/2] docs(evm): reconcile msg.value/decimals across troubleshooting, WHBAR, deploy & tutorial MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the hbar-decimals fix: the docs disagreed on whether msg.value is tinybar or wei inside a contract. This aligns the remaining pages to the verified behavior — inside the EVM, msg.value / balance / call-value are tinybar (8 decimals); the relay converts only the transaction envelope (weibar) before execution; contract-to-contract calls are NOT converted. - troubleshooting.mdx: the "Decimal handling" section stated msg.value is 18-decimal wei inside a contract and used a broken `require(msg.value >= 1 ether)` example (1 HBAR arrives as 1e8, so it always failed). Corrected to tinybar, added the two-path traps + a fixed example. - whbar.mdx: WHBAR.sol deposit() mints msg.value directly (no internal conversion; decimals = 8). Removed the claim that deposit "requires 18-decimal weibars" and fixed the contract-to-contract example from {value: 10*10**18} (=1e19 tinybar, exceeds total supply, reverts) to {value: 10*10**8} = 10 HBAR; split off-chain (weibar, relay-converted) vs contract (tinybar) paths. - deploying.mdx: note msg.value is tinybar (not wei) + link. - send-receive-hbar.mdx: add a tinybar warning explaining the existing `require(msg.value > 2000000000)` guard. Signed-off-by: Keith Kowal --- evm/development/deploying.mdx | 2 +- evm/development/troubleshooting.mdx | 11 ++++++--- evm/tokens/whbar.mdx | 23 ++++++++++--------- .../intermediate/send-receive-hbar.mdx | 4 ++++ 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/evm/development/deploying.mdx b/evm/development/deploying.mdx index dc152bba..3559f983 100644 --- a/evm/development/deploying.mdx +++ b/evm/development/deploying.mdx @@ -133,7 +133,7 @@ In Ethereum, these functions act as "catch-all" mechanisms when a contract recei #### Impacted Variables * **`msg.sender`:** The address initiating the contract call. -* **`msg.value`:** The amount of HBAR sent along with the call. +* **`msg.value`:** The HBAR sent along with the call, in **tinybar** (8 decimals) — not wei. A 1 HBAR transfer is `msg.value == 1e8`. See [Decimal Handling](/evm/differences/hbar-decimals). #### Key Points diff --git a/evm/development/troubleshooting.mdx b/evm/development/troubleshooting.mdx index d42d35eb..45599e8c 100644 --- a/evm/development/troubleshooting.mdx +++ b/evm/development/troubleshooting.mdx @@ -11,15 +11,20 @@ Hedera is EVM-compatible, but a few things behave differently than Ethereum, and ### Decimal handling: 8 vs 18 -The native Hedera ledger uses 8 decimals for HBAR (1 ℏ = 10⁸ tinybars). The JSON-RPC relay scales values up to 18 decimals so they match Ethereum's `wei` convention. Inside an EVM contract, `msg.value`, `balance`, and `gasPrice` all use 18 decimals; the relay handles the conversion. The trap is when you mix native SDK calls and EVM contract calls in the same flow. You have to do the conversion yourself there, and the off-by-`10**10` bug is easy to write. +The native Hedera ledger uses 8 decimals for HBAR (1 ℏ = 10⁸ tinybars); Ethereum tooling uses 18-decimal `wei`. The JSON-RPC relay converts a transaction's `value` and `gasPrice` from 18-decimal weibar to 8-decimal tinybar **before** the EVM runs — so **inside a contract, `msg.value`, `address(this).balance`, and the `value` in `call`/`send`/`transfer` are all tinybar (8 decimals), not wei.** Two traps: + +* Don't assume `msg.value` is wei — a 1 HBAR transfer arrives as `msg.value == 1e8`, not `1e18`. +* Contract-to-contract calls are **not** converted: `call{value: 1 ether}` sends `1e18` *as tinybar* (which exceeds the total HBAR supply and reverts). Use tinybar amounts — 1 HBAR = `1e8`. ```solidity -// Inside a contract, msg.value is in 18-decimal wei (as on Ethereum). +// msg.value is TINYBAR inside the contract: 1 HBAR = 1e8, not 1 ether (1e18). function deposit() external payable { - require(msg.value >= 1 ether, "send at least 1 HBAR"); + require(msg.value >= 1e8, "send at least 1 HBAR"); // 1e8 tinybar = 1 HBAR } ``` +See [Decimal Handling](/evm/differences/hbar-decimals) for the full explanation. + ### HBAR transfers don't always trigger `receive()` On Ethereum, sending ETH to a contract address triggers `receive()` or `fallback()`. On Hedera, a native HAPI `CryptoTransfer` (from an SDK or wallet operating at the Hedera level rather than the EVM level) credits the contract's underlying Hedera account directly. No EVM frame opens, so `receive()` doesn't run. EVM-native paths still behave normally: `call{value: ...}`, `transfer`, and internal CALL frames all invoke `receive()` / `fallback()` as expected. If you want contract logic to execute on the HAPI path, route the deposit through a `payable` call instead: diff --git a/evm/tokens/whbar.mdx b/evm/tokens/whbar.mdx index 033f5143..5372bd14 100644 --- a/evm/tokens/whbar.mdx +++ b/evm/tokens/whbar.mdx @@ -36,19 +36,20 @@ Developers can integrate WHBAR into their applications by leveraging the followi To convert HBAR into its ERC20 representation (WHBAR), use the `deposit()` function. Keep in mind that: * **Native HBAR:** Uses **8** decimal places (**tinybars**). -* **WHBAR (ERC20):** Uses **8** decimal places (**tinybars**)and *ONLY for deposits* (wrapping) uses 18 decimal places (weibars). +* **WHBAR (ERC20):** Uses **8** decimal places (**tinybars**) throughout — balances, transfers, `deposit()`, and `withdraw()`. `deposit()` simply mints an amount of WHBAR equal to the `msg.value` it receives, and `msg.value` is tinybar during EVM execution. The 18-decimal weibar form is **only** the transaction `value` an *off-chain* caller submits; the relay converts it to tinybar before `deposit()` runs. -The conversion from HBAR to WHBAR involves adjusting for these decimal differences. For example, to wrap native HBAR into WHBAR, call `deposit()` and send your HBAR as `msg.value` in weibars (10¹⁸ per HBAR): +How you specify the value depends on **who calls `deposit()`**: + +* **From off-chain tooling (ethers/viem/wallet):** set the transaction `value` in **weibar** (10¹⁸ per HBAR). The relay converts it to tinybar, so sending `10 * 10**18` weibar wraps 10 HBAR → 10 WHBAR. +* **From another contract:** the `value` is **tinybar** (no conversion) — use `10 * 10**8` for 10 HBAR. Passing `10 * 10**18` here would send 10¹⁸ tinybar, which exceeds the total HBAR supply and reverts. ```solidity wrap /** - * @notice Deposits HBAR and mints an equivalent amount of WHBAR - * @dev This is the only supported method for obtaining WHBAR + * @notice Wrap 10 HBAR into WHBAR from within a contract. + * @dev Contract-to-contract `value` is TINYBAR (8 decimals): 1 HBAR = 1e8. */ -function deposit() public payable { - // To wrap 10 HBAR into WHBAR - // Note: 1 HBAR = 10^18 weibars; conversion handles the decimal difference. - whbarContract.deposit{value: 10 * 10**18}(); +function wrapTenHbar() public { + whbarContract.deposit{value: 10 * 10**8}(); // 10 HBAR } ``` @@ -76,10 +77,10 @@ This burns 5 WHBAR and sends back 5 HBAR to the wallet. When depositing HBAR, remember the conversion nuances between decimal places. -* **Native HBAR & WHBAR Token:** 8 decimals (tinybars). -* **RPC `msg.value`:** 18 decimals (weibars). +* **Native HBAR & WHBAR token:** 8 decimals (tinybars) — including `msg.value` inside `deposit()`. +* **Transaction `value` from an off-chain caller:** 18 decimals (weibars), which the relay converts to tinybar before execution. -Although the `deposit()` function requires input in 18 decimal weibars, WHBAR tokens and all related transfers and balances use 8 decimals, identical to native HBAR in tinybars. +`deposit()` mints WHBAR equal to `msg.value`, and `msg.value` is **tinybar** during execution — WHBAR is 8-decimal throughout. The 18-decimal weibar form applies only to the `value` an *off-chain* caller puts in the transaction; a **contract** calling `deposit{value: X}` must pass tinybar (1 HBAR = `1e8`). *** diff --git a/evm/tutorials/intermediate/send-receive-hbar.mdx b/evm/tutorials/intermediate/send-receive-hbar.mdx index 4dcb20dc..535f0130 100644 --- a/evm/tutorials/intermediate/send-receive-hbar.mdx +++ b/evm/tutorials/intermediate/send-receive-hbar.mdx @@ -190,6 +190,10 @@ async function contractDeployFcn(bytecode, gasLim) { ## **Getting HBAR to the Contract** + +Inside the contract, `msg.value` and the `_amount` passed to `transfer`/`send`/`call` are in **tinybar** (8 decimals), not wei — `1 HBAR = 100_000_000`. That's why the `tokenAssociate` guard above uses `require(msg.value > 2000000000, …)` (≈20 HBAR in tinybar). Don't pass `1 ether`/wei-scale values here. See [Decimal Handling](/evm/differences/hbar-decimals). + + ### **The receive/fallback** Functions In this scenario, you (Operator) transfer 10 HBAR to the contract by triggering either the **_receive_** or **_fallback_** functions of the contract. As described in this [Solidity by Example](https://solidity-by-example.org/sending-ether/) page, the **_receive_** function is called when **_msg.data_** is empty, otherwise the **_fallback_** function is called.