
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.
| Area | Behaviour on LitVM |
|---|---|
block.number | Returns an approximate Ethereum L1 block number, not the LitVM block |
blockhash(x) | Pseudo-random and not cryptographically secure |
block.difficulty | Constant 1 |
block.prevrandao | Constant 1 |
block.coinbase | The sequencer's designated address |
msg.sender from L1 | An aliased address, not the original L1 sender |
| Block time | Variable, blocks produced on demand |
| Block gas limit | Reported 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:
| Precompile | Address | Purpose |
|---|---|---|
ArbSys | 0x64 (100) | System info: block number, chain ID |
ArbInfo | 0x65 (101) | Account info |
ArbAddressTable | 0x66 (102) | Address compression to cut calldata cost |
ArbGasInfo | 0x6c (108) | Gas pricing details |
ArbRetryableTx | 0x6e (110) | Retryable ticket management |
ArbStatistics | 0x6f (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