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)
COINBASE account readsTypically readable account stateReturns beneficiary address, but in-transaction state reads can appear empty
transactionIndexPosition in blockCan be 0 for multiple transactions in the same millisecond
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 + Turnstile-convertible stablecoin equivalent
eth_subscribe typeslogs, newHeads, newPendingTransactionslogs only
Poll-based filterseth_newFilter / eth_getFilterChangesNot 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 when the effective fee is non-zero.

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.

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.1 SBC for the Turnstile to fire.

ParameterValue
Minimum conversion0.1 SBC → 0.1 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.1 RUSD covers roughly 10,000 transactions worth of gas. If a transaction requires more than 0.1 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 + convertible SBC equivalentChecking if a wallet can pay for gas
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.1 SBC → 0.1 RUSD:

MethodRaw valueHuman-readable
balanceOf49000004.9 SBC
rad_getBalanceRaw1000000000000000000.1 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 + convertible stablecoin equivalent
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 — Simulations include Turnstile execution when the account would be short on RUSD but has sufficient SBC. This ensures gas estimates are accurate for accounts that rely on the Turnstile.

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.

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.

BLOCKHASH returns predictable values

BLOCKHASH is supported, but returns a timestamp-derived value — not a cryptographic hash. blockhash(block.number - 1) returns the previous millisecond timestamp cast to bytes32.

// INSECURE on Radius — "random" value is the previous millisecond timestamp
uint256 random = uint256(blockhash(block.number - 1));
uint256 winner = random % participants.length;

Vulnerable contract patterns:

  • Lottery and raffle contracts
  • NFT trait generation using on-chain randomness
  • Commit-reveal schemes that hash against blockhash()
  • Gaming contracts with randomized outcomes

Use Chainlink VRF, an off-chain oracle, or application-level entropy for any randomness needs.

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.
  • Radius supports eth_sendRawTransactionSync, a synchronous variant that returns the receipt directly in the send response. This eliminates the need for a separate waitForTransactionReceipt call.
  • Contracts and off-chain logic that wait for N block confirmations can treat confirmation count = 1 as final on Radius.

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.

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.

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.

One caveat:

  • transactionIndex is not always meaningful on Radius.
  • If multiple transactions share the same millisecond timestamp, receipts can report transactionIndex = 0 for each.

Practical implications:

  • 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).

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 uses blockhash() for randomness.
  5. No contract path depends on fresh beneficiary account reads during execution.
  6. Indexers and analytics do not treat transactionIndex as authoritative ordering.
  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.

Related pages