Ethereum compatibility
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
| Area | Ethereum behavior | Radius behavior |
|---|---|---|
| Gas pricing | Market-based fee bidding | Fixed gas price model |
eth_blockNumber | Monotonic block height | Current timestamp in milliseconds |
| Block storage | Canonical block chain in state | Blocks reconstructed on demand for RPC compatibility |
| Block hash | Hash of block header | Equals block number (timestamp-based value) |
COINBASE account reads | Typically readable account state | Returns beneficiary address, but in-transaction state reads can appear empty |
transactionIndex | Position in block | Can be 0 for multiple transactions in the same millisecond |
eth_getLogs filtering | Address filter optional | Address filter required |
| Block tag on state queries | Reads historical state at specified block | Accepts latest, pending, safe, finalized; returns error -32000 for historical block numbers (no archive state) |
eth_getBalance semantics | Raw native balance | Native + Turnstile-convertible stablecoin equivalent |
eth_subscribe types | logs, newHeads, newPendingTransactions | logs only |
| Poll-based filters | eth_newFilter / eth_getFilterChanges | Not supported |
| Transaction finality | Probabilistic (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.
| Parameter | Supported |
|---|---|
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}} 1000000Query 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.
| Parameter | Value |
|---|---|
| Minimum conversion | 0.1 SBC → 0.1 RUSD |
| Maximum conversion per trigger | 10.0 SBC → 10.0 RUSD |
| Conversion direction | SBC → RUSD only (one-way) |
| Gas overhead | Zero — 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.
| Method | Returns | Use when |
|---|---|---|
eth_getBalance | RUSD + convertible SBC equivalent | Checking if a wallet can pay for gas |
rad_getBalanceRaw | Raw RUSD balance only | Checking actual RUSD on hand (or whether the Turnstile will fire) |
balanceOf on SBC contract | SBC ERC-20 balance | Displaying 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^(18 − 6) = 10^12.
Worked example
An account holding 0 RUSD and 5.000000 SBC:
| Method | Raw value | Human-readable |
|---|---|---|
balanceOf | 5000000 (6 decimals) | 5.0 SBC |
rad_getBalanceRaw | 0 | 0.0 RUSD |
eth_getBalance | 5000000000000000000 (5 × 10¹⁸) | 5.0 RUSD |
After the Turnstile converts 0.1 SBC → 0.1 RUSD:
| Method | Raw value | Human-readable |
|---|---|---|
balanceOf | 4900000 | 4.9 SBC |
rad_getBalanceRaw | 100000000000000000 | 0.1 RUSD |
eth_getBalance | 5000000000000000000 (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:
| Contract | Parameter | Ethereum interpretation | Radius interpretation | Recommendation |
|---|---|---|---|---|
| Governor | votingDelay() = 1 | ~12 seconds (1 block) | 1 millisecond | Use timestamp clock mode |
| Governor | votingPeriod() = 50400 | ~1 week | ~50 seconds | Use timestamp clock mode |
| TimelockController | Block-based delay | Predictable block intervals | Millisecond timestamps | Use block.timestamp |
| Vesting contracts | Block-based schedule | Predictable duration | Duration depends on ms timestamps | Use 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
waitForTransactionReceiptreturns, 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 separatewaitForTransactionReceiptcall. - 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
addressfield return error-33014. General-purpose indexers must know contract addresses in advance. - Block range limit. The maximum range between
fromBlockandtoBlockis 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, andfinalizedreturn 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_callat 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 intervalFor 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
COINBASEas 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:
transactionIndexis not always meaningful on Radius.- If multiple transactions share the same millisecond timestamp, receipts can report
transactionIndex = 0for each.
Practical implications:
- Do not rely on
transactionIndexfor 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:
- Fee estimation returns non-zero
gasPrice. - No logic assumes Ethereum block height semantics.
- No logic relies on block hash parent-chain behavior.
- No contract uses
blockhash()for randomness. - No contract path depends on fresh beneficiary account reads during execution.
- Indexers and analytics do not treat
transactionIndexas authoritative ordering. - Log queries always include an
addressfilter. - No logic queries state at a historical block number — Radius returns error
-32000for historical state requests. Supported tags:latest,pending,safe,finalized. - Event listeners use
eth_getLogspolling or WebSocketeth_subscribe("logs")— noteth_newFilter. - Block tracking uses
eth_blockNumberpolling — noteth_subscribe("newHeads"). - Balance checks use the appropriate method (
eth_getBalancefor spendability,rad_getBalanceRawfor actual native balance,balanceOffor stablecoin balance). - Transaction confirmation logic does not wait for multiple blocks. Execution is final.