Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Ethereum compatibility

Behavior differences and RPC constraints
View as Markdown

Radius is EVM compatible. Solidity contracts, standard wallets, and Ethereum tooling work without modification. This page covers the specific areas where Radius behavior differs from Ethereum, with practical guidance for each. For tool-specific setup (Foundry, viem, Hardhat, ethers.js), see Tooling configuration.

At a glance

AreaEthereum behaviorRadius behavior
Gas pricingMarket-based fee biddingFixed gas price model
eth_blockNumberMonotonic block heightCurrent timestamp in milliseconds
Block storageCanonical block chain in stateBlocks reconstructed on demand for RPC compatibility
Block hashHash of block headerEquals block number (timestamp-based value)
On-chain randomnessprevrandao carries RANDAO mixNot a randomness source: prevrandao/difficulty are 0, blockhash predictable, no EIP-2935 — use off-chain entropy
COINBASE account readsTypically readable account stateReturns beneficiary address, but in-transaction state reads can appear empty
transactionIndexUnique per transaction within a blockAlways 0 — not a unique key; key on transactionHash
eth_getLogs filteringAddress filter optionalAddress filter required
Block tag on state queriesReads historical state at specified blockAccepts latest, pending, safe, finalized; returns error -32000 for historical block numbers (no archive state)
eth_getBalance semanticsRaw native balanceNative + full ERC-20 token holdings as native currency (uncapped; may exceed per-transaction spend limit)
eth_subscribe typeslogs, newHeads, newPendingTransactionslogs only
Poll-based filterseth_newFilter / eth_getFilterChangesNot supported
Replace-by-fee (RBF)Resubmit at a pending nonce with higher gas to cancel/replaceNonce whose tx already executed: rejected (-33009); a still-queued future nonce is replaceable with higher gas
Returned transaction hashAcknowledges the tx was accepted; not a guarantee it will mineMeans "queued" for a future-nonce tx — waits for the nonce gap to fill (evicted after ~30s if it doesn't); poll for the receipt
eth_getProofReturns Merkle state proofsNot supported
eth_getBlockReceiptsReturns all receipts in a blockNot supported
Transaction finalityProbabilistic (reorgs possible)Instant and final

Gas pricing

Radius uses a fixed gas price model.

Both legacy (gasPrice) and EIP-1559 fee fields are accepted, but any transaction whose gas price is below the fixed gas price is rejected.

ParameterSupported
gasPrice (legacy)✅ Supported
maxFeePerGas (EIP-1559)✅ Supported
maxPriorityFeePerGas (EIP-1559)✅ Supported

Current fixed gas price: ~1 gwei (9.85998816e-10 RUSD per unit of gas).

Query the current gas price with eth_gasPrice. Radius applies this value to every transaction regardless of the fee fields it specifies. eth_maxPriorityFeePerGas is supported for tooling compatibility and returns the same value (there is no separate priority-fee market); baseFeePerGas is 0, so standard EIP-1559 estimation (baseFee + priorityFee) resolves to the fixed gas price.

viem works with a standard defineChain() — no fee overrides are needed. See viem configuration for the full chain definition.

Foundry example

cast send --gas-price 1000000000 \
  --rpc-url https://rpc.testnet.radiustech.xyz \
  --account radius-deployer \
  {{CONTRACT_ADDRESS}} "transfer(address,uint256)" {{WALLET_ADDRESS}} 1000000

Query eth_gasPrice for the current fixed gas price. For comprehensive tooling setup (Foundry config files, Hardhat, wagmi, ethers.js), see Tooling configuration.

The Turnstile and balances

Radius auto-converts stablecoins to native currency for gas payments through a mechanism called the Turnstile. This changes how balance queries work compared to Ethereum.

How the Turnstile works

If an account has SBC but not enough RUSD for gas, Radius runs a zero-fee inline conversion before executing the transaction. The account must hold at least 0.01 SBC for the Turnstile to fire.

ParameterValue
Minimum conversion0.01 SBC → 0.01 RUSD
Maximum conversion per trigger10.0 SBC → 10.0 RUSD
Conversion directionSBCRUSD only (one-way)
Gas overheadZero — not charged to the caller

For a standard ERC-20 transfer, 0.01 RUSD covers roughly 1,000 transactions worth of gas. If a transaction requires more than 0.01 RUSD (for example, a large contract deployment), the Turnstile converts whatever amount is needed up to 10.0. The Turnstile also fires for native RUSD transfers that exceed the account's RUSD balance, converting enough SBC to cover the shortfall.

Three balance methods

Radius exposes three ways to query an account's balance. Each returns a different value for the same address.

MethodReturnsUse when
eth_getBalanceRUSD + full SBC holdings as native currencyChecking total gas-paying power; note: may exceed single-transaction limit
rad_getBalanceRawRaw RUSD balance onlyChecking actual RUSD on hand (or whether the Turnstile will fire)
balanceOf on SBC contractSBC ERC-20 balanceDisplaying spendable SBC

The relationship between them:

eth_getBalance = rad_getBalanceRaw + (balanceOf × 10^12)

The 10^12 multiplier bridges the decimal gap: SBC uses 6 decimals while RUSD uses 18, so 10^(186) = 10^12.

Worked example

An account holding 0 RUSD and 5.000000 SBC:

MethodRaw valueHuman-readable
balanceOf5000000 (6 decimals)5.0 SBC
rad_getBalanceRaw00.0 RUSD
eth_getBalance5000000000000000000 (5 × 10¹⁸)5.0 RUSD

After the Turnstile converts 0.01 SBC → 0.01 RUSD:

MethodRaw valueHuman-readable
balanceOf49900004.99 SBC
rad_getBalanceRaw100000000000000000.01 RUSD
eth_getBalance5000000000000000000 (unchanged)5.0 RUSD

eth_getBalance stays the same because total gas-paying power is unchanged — value moved from SBC to RUSD.

Query balances with viem

import { createPublicClient, http } from 'viem';
import { radiusTestnet } from './chain';
 
const client = createPublicClient({
  chain: radiusTestnet,
  transport: http(),
});
 
const addr = '0xYourAddress' as `0x${string}`;
 
// 1. eth_getBalance — native + full SBC token holdings as native currency (uncapped)
const ethBalance = await client.getBalance({ address: addr });
 
// 2. rad_getBalanceRaw — actual native RUSD only
const rawHex = await client.request({
  method: 'rad_getBalanceRaw' as any,
  params: [addr] as any,
});
const rusdBalance = BigInt(rawHex as string);
 
// 3. SBC balance — ERC-20 balanceOf
const sbcBalance = await client.readContract({
  address: '0x33ad9e4BD16B69B5BFdED37D8B5D9fF9aba014Fb',
  abi: [
    {
      type: 'function',
      name: 'balanceOf',
      inputs: [{ name: 'account', type: 'address' }],
      outputs: [{ type: 'uint256' }],
      stateMutability: 'view',
    },
  ] as const,
  functionName: 'balanceOf',
  args: [addr],
});

Query raw balance with rad_getBalanceRaw

curl -X POST https://rpc.testnet.radiustech.xyz \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"rad_getBalanceRaw","params":["0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18"],"id":1}'

RPC behavior with the Turnstile

The Turnstile operates automatically in the background for most applications. These methods reflect Turnstile-aware behavior:

eth_sendRawTransaction — If the account lacks RUSD but has sufficient SBC, the Turnstile executes inline before the transaction. Receipt logs include stablecoin conversion events.

eth_call and eth_estimateGas — Dry-runs skip sender balance validation unconditionally. Simulations do not fail solely because the from account lacks enough RUSD to pay for execution.

Blocks

Radius does not use blocks as the execution primitive. The atomic execution unit is a transaction.

To preserve Ethereum JSON-RPC compatibility, block-oriented responses are reconstructed on demand.

eth_blockNumber

eth_blockNumber returns the current timestamp in milliseconds (hex encoded).

Practical implications:

  • Do not treat block number as canonical chain height.
  • Do not assume "N blocks later" semantics match Ethereum finality patterns.

Affected OpenZeppelin contracts

If you are deploying governance, timelock, or vesting contracts, these differences are critical:

ContractParameterEthereum interpretationRadius interpretationRecommendation
GovernorvotingDelay() = 1~12 seconds (1 block)1 millisecondUse timestamp clock mode
GovernorvotingPeriod() = 50400~1 week~50 secondsUse timestamp clock mode
TimelockControllerBlock-based delayPredictable block intervalsMillisecond timestampsUse block.timestamp
Vesting contractsBlock-based schedulePredictable durationDuration depends on ms timestampsUse block.timestamp

OpenZeppelin v5 supports timestamp-based clock mode natively:

function CLOCK_MODE() public pure override returns (string memory) {
    return "mode=timestamp";
}
 
function clock() public view override returns (uint48) {
    return uint48(block.timestamp);
}

Block reconstruction

A reconstructed "block" contains transactions executed within the same millisecond.

Practical implications:

  • Recent block reads are compatible for tooling, but block composition is not equivalent to Ethereum's globally sequenced blocks.

Reconstructed header fields

Because blocks are reconstructed on demand rather than produced by canonical block building, several header fields carry placeholder values. Tools that validate or re-hash block headers (some clients, explorers, bridges, and test harnesses) must account for these.

FieldReconstructed value
stateRoot0x0 (all zeros) — no Merkle Patricia Trie is maintained
receiptsRoot0x0 (all zeros)
gasUsed0, even when the block contains transactions
baseFeePerGas0
miner0x0 (zero address)
transactionsRootEmpty-trie constant (see below), not 0x0

Genesis block

The genesis block (block 0x0) has a parentHash of 0x0, matching standard Ethereum behavior.

Block hashes

Block hash equals block number for a given reconstructed block (timestamp-based value), not a chained header hash.

Practical implications:

  • Do not rely on parent-hash lineage assumptions from Ethereum.
  • Do not infer canonical history from hash chaining on Radius.

On-chain randomness

On-chain values are not a secure source of randomness on any EVM chain. On Radius this is especially clear-cut: the block values Ethereum contracts sometimes use for entropy are constant or deterministic here, so they provide no unpredictability at all. Contracts ported from Ethereum that rely on them for randomness should switch to off-chain entropy.

SourceRadius behavior
block.prevrandaoConstant 0. On Ethereum this returns the beacon-chain RANDAO mix, which varies block to block; on Radius it is always 0.
block.difficultyConstant 0 (same opcode as prevrandao post-merge).
blockhash(block.number - 1)A deterministic, non-cryptographic value with no entropy — a contract can compute the identical value inside the same transaction.
blockhash for older blocksNon-zero only for block numbers within ~256 of the current one — and because block numbers are millisecond timestamps, that is only a few hundred milliseconds of history (versus ~51 minutes on Ethereum: 256 blocks × ~12s). EIP-2935's historical-hash contract is not deployed, so OpenZeppelin's Blockhash utility cannot extend past that native window: within it, it returns the predictable native blockhash value; for older blocks it returns 0.
// Predictable on Radius — not a source of randomness
uint256 random = uint256(blockhash(block.number - 1));
uint256 winner = random % participants.length;
 
// Also not random — these are constant 0 on Radius
uint256 r1 = block.prevrandao;
uint256 r2 = block.difficulty;

Because these values are known when the transaction executes, the result is fully determined in advance — a contract can compute it in the same transaction, so it provides no unpredictability.

Affected patterns:

  • Lottery and raffle contracts
  • Randomized NFT mints and trait generation
  • Games with randomized outcomes
  • Fair-ordering or commit-reveal schemes that hash against blockhash()

For any randomness need, derive entropy off-chain and bring it on-chain through a trusted path — an external randomness oracle (VRF-style), or a commit-reveal scheme whose revealed value is entropy generated off-chain. What matters is where the entropy comes from, not the commit-reveal structure itself: a commit-reveal scheme that ultimately hashes an on-chain block value is still fully predictable. Do not derive randomness from block values.

Instant finality

On Ethereum, confirmed transactions can be reorganized out of the chain. On Radius, every confirmed transaction is final.

  • When waitForTransactionReceipt returns, the transaction is final. There are no reorgs and no need to wait for additional confirmations.
  • eth_sendRawTransactionSync — the EIP-7966 synchronous variant that returns the receipt directly in the send response — eliminates the need for a separate waitForTransactionReceipt call. On Radius the returned receipt is instant and final; it never reorgs. On a typical L2 the equivalent sync receipt reflects only inclusion and can still be reorganized until it settles on L1.
  • Contracts and off-chain logic that wait for N block confirmations can treat confirmation count = 1 as final on Radius.

Transaction submission and the pseudo-mempool

Radius tries to execute every transaction immediately. If a transaction's nonce is higher than the account's current nonce, it can't execute yet, so it enters a bounded pseudo-mempool that queues such future-nonce transactions until the preceding nonces fill and they become executable. This queue behaves differently from the average Ethereum client's mempool in two ways that affect ported tools. Neither affects finality — once a transaction executes, it is final — but both change what happens at the time of submission.

Replace-by-fee and stuck-transaction recovery

For most Ethereum nodes, resubmitting a transaction at an already-occupied nonce with a higher gas price replaces the pending one. This is the basis for stuck-transaction recovery: cancelling a transaction, or bumping its fee to get it mined.

On Radius, replace-by-fee applies only to a transaction that is still queued because it specifies a future nonce (higher than the account's current nonce): resubmitting at that nonce with a higher gas price swaps in the new transaction (the same or a lower gas price is rejected). Exactly one transaction per nonce executes, and it executes at the fixed system gas price — so the higher gas price used to win the replacement is not what you actually pay.

The porting consequence: Ethereum's "fee-bump a stuck transaction" recovery targets a pending transaction at the current nonce — a state that does not exist on Radius, where a current-nonce transaction executes or is rejected immediately rather than sitting pending. You don't have to strip that recovery logic out — it simply becomes a no-op that never fires. Just be aware that a resubmission at an already-executed nonce is rejected with the generic -33009 Exec Failed, so don't treat that error as a failure of the original transaction. Rely on instant finality instead.

A returned transaction hash means "queued," not "will execute"

On Ethereum, a hash returned from eth_sendRawTransaction acknowledges that the node accepted the transaction — it is not a guarantee the transaction will mine. That caution is not Radius-specific, but the queue gives it a Radius-specific shape: a hash returned for a future-nonce transaction (one submitted while an earlier nonce is still unfilled) means only that it was accepted into the pseudo-mempool. It waits for the nonce gap to fill and executes then — and a null receipt in the meantime is the normal queued state, not a failure.

If the gap is not filled in time, the queued transaction is evicted rather than held indefinitely: Radius prunes the pseudo-mempool periodically (roughly a ~30s cycle). In testing, a queued transaction still executed when its gap was filled within ~40s, but had been dropped when the fill came later. Fill earlier nonces promptly; if a queued transaction is evicted, resubmit it.

For high-throughput submission from many accounts, keep each account's in-flight (unconfirmed) transactions low and submit in nonce order, so queued transactions can execute as earlier nonces fill.

RPC query constraints

eth_getLogs constraints

eth_getLogs on Radius differs from Ethereum in two ways:

  • Address filter required. Queries without an address field return error -33014. General-purpose indexers must know contract addresses in advance.
  • Block range limit. The maximum range between fromBlock and toBlock is 1,000,000 units. Because block numbers are millisecond timestamps, this covers ~16 minutes 40 seconds — not ~1 million blocks as on Ethereum. Queries exceeding this range return error -33002.

To query logs over longer periods, split the range into consecutive chunks of up to 1,000,000. See eth_getLogs in the method reference.

eth_getBlockReceipts is not supported

eth_getBlockReceipts returns error -33000. Radius executes transactions individually rather than in blocks — the block number returned over RPC is wall-clock time, exposed for tooling compatibility — so "every receipt in a block" is not a meaningful unit of work on Radius.

To retrieve the same data:

  • All receipts for a block — fetch the block's transactions with eth_getBlockByNumber (full transactions), then call eth_getTransactionReceipt for each. This is the direct equivalent of one eth_getBlockReceipts call.
  • Event indexing over a range — query eth_getLogs with an address filter (see eth_getLogs constraints). This is the efficient path for tracking known contracts, but returns logs only — not full receipts.

No historical state access

eth_getBalance, eth_getTransactionCount, eth_getCode, eth_call, eth_getStorageAt, and eth_estimateGas parse block tags correctly:

  • latest, pending, safe, and finalized return current state.
  • Historical block numbers return error -32000: "required historical state unavailable".

Radius does not support archive mode. There is no way to read state at a past block.

Practical implications:

  • Foundry fork mode (--fork-block-number) returns an error — past state is not available.
  • eth_call at a past block returns an error instead of silently returning current state.
  • Indexers and analytics that query historical balances receive clear errors rather than misleading data.

eth_getProof is not supported

eth_getProof returns error -33000. Radius stores state across a parallelized, sharded infrastructure with no single global Merkle-Patricia trie, so it does not issue state proofs — and its instant, deterministic finality removes the need for them.

Read account and storage values directly with eth_getBalance, eth_getCode, and eth_getStorageAt.

WebSocket subscriptions support logs only

eth_subscribe("logs") works correctly with live notifications and fully populated log fields.

import { createPublicClient, webSocket } from 'viem';
import { radiusTestnet } from './chain';
 
const client = createPublicClient({
  chain: radiusTestnet,
  transport: webSocket('wss://rpc.testnet.radiustech.xyz'),
});
 
const unwatch = client.watchContractEvent({
  address: contractAddress,
  abi: contractAbi,
  eventName: 'Transfer',
  onLogs: (logs) => {
    // process logs
  },
});

eth_subscribe("newHeads"), eth_subscribe("newPendingTransactions"), and eth_subscribe("syncing") return error -32602.

For block tracking, poll eth_blockNumber on a 10–30 second interval.

Poll-based filters are not supported

eth_newFilter, eth_newBlockFilter, eth_newPendingTransactionFilter, eth_getFilterChanges, and eth_getFilterLogs are unsupported.

For event monitoring over HTTP, poll eth_getLogs with incrementing fromBlock:

let lastBlock = await client.getBlockNumber();
 
setInterval(async () => {
  const currentBlock = await client.getBlockNumber();
  if (currentBlock <= lastBlock) return;
 
  const logs = await client.getLogs({
    address: contractAddress,
    fromBlock: lastBlock + 1n,
    toBlock: currentBlock,
  });
  lastBlock = currentBlock;
  // process logs
}, 15_000); // 15-second interval

For real-time events, use WebSocket eth_subscribe("logs").

Beneficiaries and COINBASE (0x41)

Radius pays fees to beneficiary addresses.

COINBASE returns the beneficiary address for the current execution environment. However, state reads of beneficiary accounts during execution can return an empty account view.

eth_getBalance can still return beneficiary balance views, but those reads are ephemeral and can be stale.

Practical implications:

  • Do not write contract logic that depends on fresh in-transaction beneficiary account state.
  • Treat COINBASE as an identifier, not as a reliable in-transaction state anchor.

Stored transaction data and receipts

Receipts are broadly Ethereum-like. Contract deployment receipts set to to null and all log fields (topics, data, logIndex, address) are fully populated, matching standard Ethereum behavior.

Three caveats:

  • transactionIndex is always 0 on Radius. Because transactions execute in parallel, Radius does not assign a position within the reconstructed block, so every transaction and log reports transactionIndex = 0 and the value carries no ordering information.
  • eth_getTransactionReceipt may briefly return null for a transaction that has just been sent, before the receipt is committed to state (seen as premature-poll failures in tools like Foundry rather than measured directly). Poll rather than treating a single null as failure; the window is transient and a returned receipt indicates a transaction has finalized.
  • eth_getLogs visibility lags receipt availability by ~100 ms. A log can be missing from eth_getLogs results for a brief window after its eth_getTransactionReceipt already returns.

Practical implications:

  • (blockNumber, transactionIndex) is not a unique key on Radius. Key on transactionHash instead. Indexers, explorers, or databases that treat (blockNumber, transactionIndex) as a unique row identifier — a common Ethereum assumption — will map multiple same-millisecond transactions to the same key; depending on the storage layer, those rows are overwritten or dropped.
  • Do not rely on transactionIndex for ordering. Use your own ordering keys (for example: ingestion sequence, application-level nonce tracking, or timestamp plus transaction hash).
  • Do not assume a log is queryable via eth_getLogs the instant its receipt exists. Re-scan the range on a subsequent pass to pick up events emitted within the visibility lag.
  • Poll for a receipt (waitForTransactionReceipt) rather than treating a single null read as failure; it resolves once the receipt is served.

Integration checklist

Before shipping to production, verify:

  1. Fee estimation returns non-zero gasPrice.
  2. No logic assumes Ethereum block height semantics.
  3. No logic relies on block hash parent-chain behavior.
  4. No contract derives randomness from on-chain values (blockhash, block.prevrandao, block.difficulty) — all are predictable or constant on Radius. Use off-chain entropy.
  5. No contract path depends on fresh beneficiary account reads during execution.
  6. Indexers and analytics do not treat transactionIndex as authoritative ordering, and key on transactionHash — not (blockNumber, transactionIndex), which is not unique on Radius. Indexers also re-scan eth_getLogs rather than assume a log is queryable the instant its receipt exists.
  7. Log queries always include an address filter.
  8. No logic queries state at a historical block number — Radius returns error -32000 for historical state requests. Supported tags: latest, pending, safe, finalized.
  9. Event listeners use eth_getLogs polling or WebSocket eth_subscribe("logs") — not eth_newFilter.
  10. Block tracking uses eth_blockNumber polling — not eth_subscribe("newHeads").
  11. Balance checks use the appropriate method (eth_getBalance for spendability, rad_getBalanceRaw for actual native balance, balanceOf for stablecoin balance).
  12. Transaction confirmation logic does not wait for multiple blocks. Execution is final.
  13. No integration depends on eth_getProof (state proofs) or eth_getBlockReceipts; both return error -33000. Use direct state reads and eth_getTransactionReceipt / eth_getLogs instead.
  14. No stuck-transaction recovery depends on fee-bumping a transaction at the current nonce — that Ethereum pattern is a harmless no-op on Radius (a current-nonce tx executes or is rejected immediately; rely on instant finality). Don't treat a -33009 Exec Failed on resubmission as failure of the original. Replace-by-fee works only for a transaction still queued behind an unfilled future nonce, and will only be accepted in the mempool if defined with a higher gas price.
  15. A returned transaction hash is treated as "queued," not "will execute." Poll for the receipt (or use eth_sendRawTransactionSync) and fill earlier nonces promptly — a queued future-nonce transaction is evicted (~30s) if the gap is not filled.

Related pages