Developer Quickstart
Welcome to the Scroll Developer Quickstart. This guide walks you through building a minimal on-chain app, from installing tooling to deploying contracts on Scroll and connecting it to a React frontend.
What You’ll Build
By the time you’re done, you’ll have:
- Installed developer tooling.
- Deployed a Counter smart contract on Scroll by using Foundry.
- Created a React frontend (with wagmi and Viem) to read from and write to your contract.

What you’ll build: a hello world dApp on Scroll connecting a React frontend to a contract you deployed.
Why Scroll?
Scroll is a high performance, EVM equivalent zkEVM Layer 2 designed to help developers build secure, low-cost, and engaging applications. Because Scroll is fully bytecode compatible with the EVM, your existing development and testing tools work out of the box, just configure them to use a Scroll RPC provider.
If you run into any issues, please reach out in our Discord.
Install and Configure Foundry
We’ll use Foundry to compile and deploy our Counter contract.
Create a folder for the contract:
# create a new contracts directorymkdir contractscd contracts# install and update foundry if you haven'tcurl -L https://foundry.paradigm.xyz | bashfoundryup# initialize a fresh Foundry projectforge init --no-git
Configure Environment Variables
To deploy your smart contracts on Scroll, you need two key components:
- An RPC node connection to interact with the Scroll network: https://sepolia-rpc.scroll.io/
- A funded private key to deploy the contract
Let’s set up both of these:
.env
SCROLL_SEPOLIA_RPC_URL="https://sepolia-rpc.scroll.io/"PRIVATE_KEY=<YOUR_PRIVATE_KEY>
Also be sure you don’t upload this to a public repo by setting up a .gitignore
.
.gitignore
.env
If you don’t have Sepolia test ETH yet, join our Telegram faucet and send /drop YOUR_ADDRESS
to receive Sepolia ETH on Scroll.
Deploy Your Smart Contract
With Foundry set up and .env
in place, deploy the Counter contract.
In the contracts directory, run:
source .envforge create src/Counter.sol:Counter \ --rpc-url $SCROLL_SEPOLIA_RPC_URL \ --broadcast \ --private-key $PRIVATE_KEY
After deployment, you’ll see something like:
[⠊] Compiling...No files changed, compilation skippedDeployer: 0xbef34f2FCAe62dC3404c3d01AF65a7784c9c4A19Deployed to: 0xf0e9ceCAE516B2F5Ac297B3857453b07455f817FTransaction hash: 0x2bca5934ad82ce26332847fdd6a9241b0da0e38e6928f06499ee58ebc967bbde
Copy the address under Deployed to:
. You’ll need it when configuring the frontend.
Create and Configure the React Frontend
We’ll build a simple React app (using Vite, wagmi, and Viem) that connects to MetaMask (or another injected provider) and interacts with the Counter contract.
From the root of your project:
cd ..pnpm create wagmi frontend -t vite-reactcd frontendpnpm install
This scaffolds a new Vite + React + wagmi template.
Now add the smart contract address you just deployed to the .env.local
file.
.env.local
VITE_COUNTER_CONTRACT_ADDRESS=0xYourNewContractAddress
Implement the Frontend Logic
Create a new Counter
component on src/components/Counter.tsx
that is able to read and write to our smart contract.
src/components/Counter.tsx
import { useAccount, useWriteContract } from 'wagmi'import { createPublicClient, http } from 'viem'import { scrollSepolia } from 'viem/chains'import { useState, useEffect } from 'react'
const COUNTER_CONTRACT_ADDRESS = import.meta.env.VITE_COUNTER_CONTRACT_ADDRESS as `0x${string}`
// Connect your client to Scroll Sepoliaconst publicClient = createPublicClient({ chain: scrollSepolia, transport: http()})
// Define the ABI so we can interact with the contract from TSconst counterABI = [ { inputs: [], name: "increment", outputs: [], stateMutability: "nonpayable", type: "function", }, { inputs: [], name: "number", outputs: [{ type: "uint256" }], stateMutability: "view", type: "function", },] as const
// Custom hook for counter functionalityfunction useCounter() { const [number, setNumber] = useState<bigint | null>(null) const { writeContract, isPending: isIncrementing } = useWriteContract() const fetchNumber = async () => { try { const result = await publicClient.readContract({ address: COUNTER_CONTRACT_ADDRESS, abi: counterABI, functionName: 'number', }) setNumber(result) } catch (error) { console.error('Error reading contract:', error) } } useEffect(() => { fetchNumber() }, []) const increment = () => { writeContract({ address: COUNTER_CONTRACT_ADDRESS, abi: counterABI, functionName: 'increment', }, { onSuccess: (txHash) => { publicClient.waitForTransactionReceipt({ hash: txHash }) .then(() => fetchNumber()) .catch(console.error) }, }) } return { number, isIncrementing, increment, refreshNumber: fetchNumber }}// Counter Componentexport function Counter() { const account = useAccount() const { number, isIncrementing, increment } = useCounter() return ( <div style={{ marginTop: '20px' }}> <h2>Counter</h2> <p>Current number: {number?.toString()}</p> {account.status === 'connected' && ( <button onClick={increment} disabled={isIncrementing} > {isIncrementing ? 'Incrementing...' : 'Increment'} </button> )} </div> )}
Now let’s add our new component into src/App.tsx
.
import { useAccount, useConnect, useDisconnect } from 'wagmi'// Import our newly created componentimport { Counter } from './components/Counter'
function App() { const account = useAccount() const { connectors, connect, error } = useConnect() const { disconnect } = useDisconnect()
return ( <div style={{ padding: '20px' }}> <div> <h2>Wallet</h2> {account.status === 'connected' ? ( <> <p>Connected: {account.addresses?.[0]}</p> <button onClick={() => disconnect()}>Disconnect</button> </> ) : ( <> {connectors.map((connector) => ( <button key={connector.uid} onClick={() => connect({ connector })} style={{ marginRight: '10px' }} > Connect {connector.name} </button> ))} {error && <p style={{ color: 'red' }}>{error.message}</p>} </> )} </div>
{/* Use the Counter component in our dApp */} <Counter /> </div> )}
export default App
Start the Frontend
Start your development server:
pnpm run dev
Open your browser at http://localhost:5173 or whatever Vite shows. You should see wallet connection buttons and an increment button. First, click the connect wallet button corresponding to the wallet you installed and then click the increment button to see the number going up.
Ready to go live?
When you’re ready to switch to mainnet, simply replace the Sepolia RPC URL with the Scroll mainnet RPC URL in your environment: https://rpc.scroll.io/
. In your React code, update the Viem client to use scroll
instead of scrollSepolia
:
import { scroll } from 'viem/chains'
const publicClient = createPublicClient({ chain: scroll, transport: http(),})
This change tells Viem to point at the live Scroll chain rather than Sepolia. All of your existing contract reads and writes will now go to mainnet.