Skip to main content

Smart Contracts

XPHERE is fully EVM-compatible at the Cancun upgrade level. Any contract that compiles for Ethereum mainnet runs on XPHERE without modification.

Toolchains​

Hardhat​

Recommended for most TypeScript-based teams. See the Quickstart for a complete walkthrough.

Foundry​

# foundry.toml
[rpc_endpoints]
xphere_mainnet = "https://rpc.x-phere.com"
xphere_testnet = "https://testnet.x-phere.com"

Deploy with:

forge create Counter --rpc-url xphere_testnet --private-key $PRIVATE_KEY

For Mainnet source verification, point Foundry at XPScan — see Verifying Contracts.

Remix (Browser)​

  1. Open remix.ethereum.org
  2. Compile with Solidity ≥ 0.8.20
  3. Deploy & Run → Environment → Injected Provider - MetaMask
  4. Switch MetaMask to XPHERE (see Wallet Setup)
  5. Click Deploy

Gas Pricing​

FieldValueNotes
baseFeePerGasDynamic (EIP-1559)Adjusts every block based on network demand
Priority fee (tip)0eth_maxPriorityFeePerGas returns 0x0 on both networks — transactions are included with a zero tip. No minimum is enforced
Block gas limit0xe8d4a50fff (999,999,999,999)Read from eth_getBlockByNumber. Far above Ethereum's 30M
dgp.gastarget30,000,000The EIP-1559 gas target used for base-fee adjustment — not the block limit

Confirm either value yourself:

curl -s -X POST https://rpc.x-phere.com -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_maxPriorityFeePerGas","params":[],"id":1}'
# {"jsonrpc":"2.0","id":1,"result":"0x0"}

Estimate gas before submitting:

const estimated = await contract.increment.estimateGas();
const tx = await contract.increment({ gasLimit: estimated * 12n / 10n }); // +20% buffer

Verifying Contracts​

XPScan exposes an Etherscan-compatible verification API, so npx hardhat verify and forge verify-contract both work on XPHERE Mainnet. The API and the flow below were run end to end against a contract deployed on Mainnet before being documented here.

Hardhat​

Add XPScan as a custom chain in hardhat.config.ts:

etherscan: {
apiKey: { xphereMainnet: "any" },
customChains: [{
network: "xphereMainnet",
chainId: 20250217,
urls: {
apiURL: "https://xpscan.io/api",
browserURL: "https://xpscan.io",
},
}],
}

No API key is issued or needed — the parameter is accepted and ignored, so any string works.

Verify after deploying, passing the constructor arguments in declaration order:

npx hardhat verify --network xphereMainnet 0xYourContract "constructorArg"
Successfully submitted source code for contract
contracts/Counter.sol:Counter at 0xYourContract
for verification on the block explorer. Waiting for verification result...

Successfully verified contract Counter on the block explorer.
https://xpscan.io/address/0xYourContract#code
A deprecation warning is expected

hardhat-verify prints a warning about network-specific API keys and the Etherscan v2 API. XPScan implements the v1 request format, which is what custom chains use, so the warning is harmless and verification succeeds.

Foundry​

forge verify-contract 0xYourContract src/Counter.sol:Counter \
--chain-id 20250217 \
--verifier-url https://xpscan.io/api \
--etherscan-api-key any

How it behaves​

  • Verification is asynchronous. Submission returns a GUID immediately and the result is polled. Compilation runs server-side, and the compiler is fetched on demand if that build has not been used before, so a first submission for an unusual compiler version takes longer.
  • Re-submitting a verified address returns Contract source code already verified.
  • solidity-single-file submissions do not populate the ABI. That path does not extract one, so getabi returns nothing for contracts verified that way. Verify with standard-JSON input — which is what hardhat verify sends — if you need the ABI to be queryable.
  • The compiler version must include the commit hash. v0.8.24 is rejected; v0.8.24+commit.e11b9ed9 is accepted. Bytecode cannot be reproduced without the exact build.

Checking a contract from the API​

# ABI of a verified contract
curl "https://xpscan.io/api?module=contract&action=getabi&address=0xYourContract"

# Full verification record — source, compiler, optimizer, constructor args
curl "https://xpscan.io/api?module=contract&action=getsourcecode&address=0xYourContract"

An unverified address returns {"status":"0","message":"NOTOK","result":"Contract source code not verified"}.

Manual verification​

For contracts deployed without a local build — or from Remix — the web form at xpscan.io/verify does the same job by hand.

The submitted source must reproduce the deployed bytecode exactly, using the compiler settings used at deployment:

FieldRequiredWhat to supply
Contract addressYesThe deployed address
Contract nameNoThe Solidity contract name, e.g. Counter
Compiler versionYesPicked from a dropdown of exact builds
LicenseYesMIT, Apache-2.0, GPL-3.0, and the other SPDX options
OptimizationYesOn or Off — must match the compile-time setting
Solidity source codeYesThe full source, in a single field
ABI-encoded constructor argumentsOnly if the constructor takes argumentsThe encoded argument blob

The form accepts one source field, so a multi-file project has to be flattened first (npx hardhat flatten or forge flatten). The CLI path above has no such limitation.

Testnet​

XPScan serves Mainnet only — its footer reports Chain ID 20250217, and no testnet instance is published. Testnet contracts are verified through the Tamsa testnet explorer's form at xpt.tamsa.io/main/verifyContract; there is no Etherscan-compatible API for testnet, so CLI verification is Mainnet-only.

Pre-deployed Standards​

Battle-tested OpenZeppelin contracts are not auto-deployed — install via npm and inherit:

npm install @openzeppelin/contracts
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyToken is ERC20 {
constructor() ERC20("MyToken", "MTK") {
_mint(msg.sender, 1_000_000 * 10 ** decimals());
}
}

Common Patterns​

Reading on-chain state from a backend​

import { createPublicClient, http } from "viem";
import { xphere } from "./chains";

const client = createPublicClient({ chain: xphere, transport: http() });
const count = await client.readContract({
address: "0x...",
abi: counterAbi,
functionName: "count",
});

Subscribing to events (WebSocket)​

import { createPublicClient, webSocket } from "viem";

const client = createPublicClient({
chain: xphere,
transport: webSocket("wss://rpc.x-phere.com/ws"),
});

client.watchContractEvent({
address: "0x...",
abi: counterAbi,
eventName: "Incremented",
onLogs: (logs) => console.log(logs),
});

See Also​