Skip to content
LitVM AcademyStart
All modules
Dev Hammer badge, locked

Module 07 of 07

Building on LitVM

Ship a contract with Foundry or Hardhat, then handle the EVM differences that break naive Ethereum code.

14 min read, then 10 questions

LitVM is EVM equivalent, so almost everything you know transfers directly. Almost. This module covers the deployment path first, then the handful of behavioural differences that silently break code ported from Ethereum.

Your existing toolchain works

Solidity and Vyper compile as normal. Hardhat, Foundry, Remix, and Truffle all work. ethers.js, viem, and web3.js all connect. ERC-20, ERC-721, and ERC-1155 behave as specified. MetaMask, Rabby, Coinbase Wallet, and WalletConnect all sign.

The only thing you configure is the network.

Foundry

Add the chain to foundry.toml:

[rpc_endpoints]
litvm = "https://liteforge.rpc.caldera.xyz/http"

[etherscan]
litvm = { key = "", url = "https://liteforge.explorer.caldera.xyz/api" }

Deploy:

forge create \
  --rpc-url https://liteforge.rpc.caldera.xyz/http \
  --private-key $PRIVATE_KEY \
  src/Counter.sol:Counter

Call and send with cast:

# read
cast call $ADDR "count()(uint256)" --rpc-url litvm

# write
cast send $ADDR "increment()" --private-key $PRIVATE_KEY --rpc-url litvm

Hardhat

// hardhat.config.ts
import type { HardhatUserConfig } from "hardhat/config";

const config: HardhatUserConfig = {
  solidity: "0.8.24",
  networks: {
    litvm: {
      url: "https://liteforge.rpc.caldera.xyz/http",
      chainId: 4441,
      accounts: [process.env.PRIVATE_KEY!],
    },
  },
};

export default config;
npx hardhat run scripts/deploy.ts --network litvm

Remix

For a one-off contract, Remix is the shortest path. Select Injected Provider as the environment, make sure the connected wallet is on LiteForge, and deploy. The wallet supplies the network, so there is nothing else to configure.

The differences that will bite you

This is the section to read twice. LitVM inherits Arbitrum's behaviour, and Arbitrum differs from Ethereum in ways that compile fine and then misbehave.

AreaBehaviour on LitVM
block.numberReturns an approximate Ethereum L1 block number, not the LitVM block
blockhash(x)Pseudo-random and not cryptographically secure
block.difficultyConstant 1
block.prevrandaoConstant 1
block.coinbaseThe sequencer's designated address
msg.sender from L1An aliased address, not the original L1 sender
Block timeVariable, blocks produced on demand
Block gas limitReported as 1,125,899,906,842,624, effective cap 32,000,000

Block numbers

block.number inside a contract returns a value close to the Ethereum L1 block at which the sequencer received the transaction. It is not the LitVM block number, it lags slightly, and several LitVM blocks can share one L1 block.

To get the real rollup block number, use the ArbSys precompile:

interface ArbSys {
    function arbBlockNumber() external view returns (uint256);
    function arbBlockHash(uint256 blockNumber) external view returns (bytes32);
    function arbChainID() external view returns (uint256);
}

contract Example {
    function litvmBlock() external view returns (uint256) {
        return ArbSys(address(100)).arbBlockNumber();
    }
}

From the RPC side, a receipt carries both: receipt.blockNumber is the LitVM block and receipt.l1BlockNumber is the approximate Ethereum block.

Randomness

blockhash() on LitVM is not a secure source of randomness. Any contract that uses it to pick a winner, shuffle, or assign traits is exploitable.

Use a dedicated oracle or a commit-reveal scheme instead. This is not a LitVM quirk so much as an L2 reality, but the failure is silent and the money is real.

Timing

Timestamps come from the sequencer's clock. They increase monotonically and are bounded to within 24 hours in the past and 1 hour in the future. Blocks are produced on demand rather than on a fixed interval.

The practical rule: long horizon timing measured in hours or days is fine, and short horizon timing measured in minutes or blocks is not. Do not assume a fixed seconds-per-block.

Gas and fees

Every transaction pays two components: L2 execution cost and the cost of posting data to the settlement layer. This is why the reported block gas limit looks absurd while the effective execution cap is 32 million.

eth_estimateGas accounts for both and works normally. Estimates move with L1 gas prices, so test under different conditions before hard-coding a limit. When analysing a block, read gasUsed, not gasLimit.

Address aliasing

A contract on the settlement layer calling into LitVM does not arrive as itself:

L2_Alias = L1_Address + 0x1111000000000000000000000000000000001111

This prevents collision attacks where an L1 contract impersonates an L2 address. If your contract accepts cross-chain messages, it must apply the offset before comparing addresses.

Precompiles

Standard Ethereum precompiles are all present, plus Arbitrum's:

PrecompileAddressPurpose
ArbSys0x64 (100)System info: block number, chain ID
ArbInfo0x65 (101)Account info
ArbAddressTable0x66 (102)Address compression to cut calldata cost
ArbGasInfo0x6c (108)Gas pricing details
ArbRetryableTx0x6e (110)Retryable ticket management
ArbStatistics0x6f (111)Chain statistics

Blocks also carry extra fields: l1BlockNumber, sendCount, and sendRoot. Transaction types 100 through 106 cover deposits, cross-chain calls, and retryable tickets.

Infrastructure you can build against

DIA oracles

Price feeds are already deployed on the testnet, exposed through the familiar AggregatorV3Interface shape.

interface IAggregator {
    function latestRoundData() external view returns (
        uint80 roundId,
        int256 answer,
        uint256 startedAt,
        uint256 updatedAt,
        uint80 answeredInRound
    );
}

contract PriceReader {
    // LTC/USD adapter on LiteForge testnet
    address constant LTC_USD = 0x45dDa5d881BD2C917976CCfde74fFd6f6412da29;

    function ltcPrice() external view returns (int256 price, uint256 updatedAt) {
        (, price, , updatedAt, ) = IAggregator(LTC_USD).latestRoundData();
    }
}

Answers carry 18 decimals. Feeds update on a 1 hour heartbeat or a deviation threshold of 0.5% for stablecoins and 1% for everything else. Available on testnet: USDC, USDT, ETH, BTC, LTC/USD, XAU/USD, XAG/USD, WTI/USD, XBR/USD.

Safe

Safe multisig is deployed on LiteForge by Protofire. Create one at app.safe.global, select LitVM Testnet, configure signers and threshold, and fund it with zkLTC for gas. Use it for contract ownership rather than holding upgrade rights on a single key.

Goldsky

Indexing via subgraphs and Turbo pipelines, for when reading events straight from the RPC stops scaling.

Where to ship

The LiteForge hackathon runs four tracks: Litecoin-focused DeFi, AI agents and agentic apps, RWA and real world utility, and an open track. Judging weighs innovation, Hard Money Web3 alignment, technical quality, and UX. Submissions go through the LVC Discord.

Beyond that, 51% of $LITVM supply is earmarked for community and ecosystem via the Litecoin DAO, which includes grants and bootstrapping capital.

What to remember

  • 01LitVM is EVM equivalent: Solidity, Foundry, Hardhat, Remix, ethers, and viem all work with only a network config change.
  • 02block.number returns an approximate Ethereum L1 block, not the LitVM block. Use ArbSys at address 100 for the real one.
  • 03blockhash() is not cryptographically secure on LitVM. Use an oracle or commit-reveal for randomness.
  • 04block.difficulty and block.prevrandao are both the constant 1.
  • 05Block time is variable, so never assume a fixed seconds-per-block for time-sensitive logic.
  • 06The reported block gas limit is misleading; the effective execution cap is 32 million.
  • 07L1 to L2 messages arrive with msg.sender aliased by adding 0x1111...1111.
  • 08DIA price feeds, Safe multisig, and Goldsky indexing are already live on the testnet.

Primary sources

Ready for the Dev Hammer?

Ten questions on this module. Answer 8 correctly and the badge unlocks. You can retake it as often as you like.

Take the quiz