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.
image

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 directory
mkdir contracts
cd contracts
# install and update foundry if you haven't
curl -L https://foundry.paradigm.xyz | bash
foundryup
# initialize a fresh Foundry project
forge init --no-git

Configure Environment Variables

To deploy your smart contracts on Scroll, you need two key components:

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 .env
forge 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 skipped
Deployer: 0xbef34f2FCAe62dC3404c3d01AF65a7784c9c4A19
Deployed to: 0xf0e9ceCAE516B2F5Ac297B3857453b07455f817F
Transaction 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-react
cd frontend
pnpm 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 Sepolia
const publicClient = createPublicClient({
chain: scrollSepolia,
transport: http()
})
// Define the ABI so we can interact with the contract from TS
const 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 functionality
function 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 Component
export 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 component
import { 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.

What's Next

Stay up-to-date on the latest Scroll Developer news
Roadmap updates, virtual and live events, ecosystem opportunities and more
Thank you for subscribing!

Resources

Follow Us