# Docs for your LLM Source: https://docs.scroll.io/en/developers/ Learn how to build with AI using these APIs. Our docs are available in a text format. You can use this to feed your LLM with the docs. [https://docs.scroll.io/en/developers/](https://docs.scroll.io/en/developers/) # Building on Scroll | Scroll Documentation ## https://docs.scroll.io/en/developers/ Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer Building on Scroll On This PageGetting StartedWhy Build on Scroll?Thank you for building with us. Welcome to the Scroll developer documentation! Scroll is its own Layer 2 network built on Ethereum (more specifically, a “zero knowledge rollup”). If you’re experienced in building on Ethereum, your code, dependencies, and tooling work with Scroll out of the box. This is possible because our network is compatible with EVM bytecode and designed to feel just like developing on Ethereum. New to zero knowledge rollups? Scroll gains its security and speed by executing transactions off-chain, and also producing a cryptographic proof that the transactions were executed correctly. This cryptographic proof is verified in a smart contract on Layer 1, ensuring that all code executed on the Scroll Layer 2 behaves just as if it were executed on Ethereum Layer 1.Learn more about Scroll’s architecture → Getting Started Looking to build on the Scroll? For the essentials: Check out the Developer Quickstart For a tutorial walking through deploying your first smart contract on Scroll, read our contract deployment tutorial We also have a number of deployed contract addresses to build on. Why Build on Scroll? Throughput — Scroll creates more secure blockspace for Ethereum. ZK Rollups allow for more activity on the network, minimizing congestion. By inheriting the security of Ethereum, which verifies the behavior of the network using zero knowledge proofs, Scroll can process more transactions without compromising on decentralization. Cost — Scroll saves users gas fees. On Ethereum, competition for blockspace results in higher costs per transaction, as each transaction makes a bid to be included in the next block. Scroll leverages recent breakthroughs in zero knowledge proofs and hardware acceleration to vastly increase secure blockspace and minimize transaction costs for users. Speed — Scroll delivers feedback to users, faster. After the merge, Ethereum blocks reliably confirm every 12 seconds. Scroll blocks are minted every 3 seconds, and for the sake of lower-risk operations, transactions can be assumed to be final once included in a block. This opens up new possibilities for on-chain interaction in social and gaming applications. Alignment — Scroll builds on Ethereum’s vision. Scroll builds on Ethereum’s vision. Our ethos is to build Ethereum, not to splinter it. Decentralization, permissionlessness, censorship-resistance, and community ownership are the core of what we do and the roadmap we’re building. We believe in open-source software, and we work closely with the Ethereum Foundation’s Privacy and Scaling Explorations team to support their work on a zkEVM that might someday be the heart of Ethereum.We also work with governance DAOs and other open-source protocols to make sure that as applications are deployed, we’re working to grow their impact — whether that be in public goods, core infrastructure, or the next generation of zero knowledge use cases. Community — Scroll brings together users and builders. We know the challenges of building in the open and getting user engagement! Scroll has a blossoming community of users and builders, and with a Discord community of over 500,000 members eager to try out applications on our testnet or mainnet, we’re excited to connect builders with users who can provide real-world feedback. Thank you for building with us. Our mainnet is now live, and we’re diligently working to bring more integrations and support infrastructure to the network. Join our growing developer community. You can find us on Discord, join our discussion forum, or follow our progress on Twitter. What's Next Developer Quickstart MoreEdit this pageJoin our community On This PageGetting StartedWhy Build on Scroll?Thank you for building with us. MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us # Developer Quickstart | Scroll Documentation ## https://docs.scroll.io/en/developers/developer-quickstart Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer Developer Quickstart On This PageWhat You’ll BuildWhy Scroll?Install and Configure FoundryConfigure Environment VariablesDeploy Your Smart ContractCreate and Configure the React FrontendImplement the Frontend LogicStart the FrontendReady to go live? 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 Setting Up Your Wallet New to Web3? Start by installing Rabby or MetaMask. After creating your wallet and securing your seed phrase, head over to ChainList to add Scroll Sepolia to your wallet with a single click. Once you’re set up, you can export your private key for deployment by following MetaMask’s guide. Let’s set up both of these: .env SCROLL_SEPOLIA_RPC_URL="https://sepolia-rpc.scroll.io/"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. Security Warning Never share or commit your private key. Always keep it secure and handle with care. 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(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 (

Counter

Current number: {number?.toString()}

{account.status === 'connected' && ( )}
)} 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 (

Wallet

{account.status === 'connected' ? ( <>

Connected: {account.addresses?.[0]}

) : ( <> {connectors.map((connector) => ( ))} {error &&

{error.message}

} )}
{/* Use the Counter component in our dApp */}
)} 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 Verify Your Smart Contracts MoreEdit this pageJoin our community On This PageWhat You’ll BuildWhy Scroll?Install and Configure FoundryConfigure Environment VariablesDeploy Your Smart ContractCreate and Configure the React FrontendImplement the Frontend LogicStart the FrontendReady to go live? MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us ```plaintext # 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 ``` ```plaintext SCROLL_SEPOLIA_RPC_URL="https://sepolia-rpc.scroll.io/"PRIVATE_KEY= ``` ```plaintext .env ``` ```plaintext source .envforge create src/Counter.sol:Counter \ --rpc-url $SCROLL_SEPOLIA_RPC_URL \ --broadcast \ --private-key $PRIVATE_KEY ``` ```plaintext [⠊] Compiling...No files changed, compilation skippedDeployer: 0xbef34f2FCAe62dC3404c3d01AF65a7784c9c4A19Deployed to: 0xf0e9ceCAE516B2F5Ac297B3857453b07455f817FTransaction hash: 0x2bca5934ad82ce26332847fdd6a9241b0da0e38e6928f06499ee58ebc967bbde ``` ```plaintext cd ..pnpm create wagmi frontend -t vite-reactcd frontendpnpm install ``` ```plaintext VITE_COUNTER_CONTRACT_ADDRESS=0xYourNewContractAddress ``` ```plaintext 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(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 (

Counter

Current number: {number?.toString()}

{account.status === 'connected' && ( )}
)} ``` ```plaintext 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 (

Wallet

{account.status === 'connected' ? ( <>

Connected: {account.addresses?.[0]}

) : ( <> {connectors.map((connector) => ( ))} {error &&

{error.message}

} )}
{/* Use the Counter component in our dApp */}
)} export default App ``` ```plaintext pnpm run dev ``` ```plaintext import { scroll } from 'viem/chains' const publicClient = createPublicClient({ chain: scroll, transport: http(),}) ``` # Verifying Smart Contracts | Scroll Documentation ## https://docs.scroll.io/en/developers/verifying-smart-contracts Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer Verifying Smart Contracts On This PageUsing Developer ToolsHardhatFoundry After deploying your smart contracts, it’s important to verify your code on a block explorer. This can be done in an automated way using your developer tooling or the Web UI. Using Developer Tools Most smart contract tooling has plugins for verifying your contracts easily on Etherscan. Scroll Mainnet: https://api.scrollscan.com/api Scroll Sepolia: https://api-sepolia.scrollscan.com/api Using the Scrollscan API When using Scrollscan, you will need to register an account and create an API key. The same key can be used for both Scroll Sepolia and mainnet. After creating your API key, please wait a few minutes before it comes into effect. Hardhat Modify hardhat.config.ts to point to Scroll’s RPC and block explorer API. For example, the config for Scroll Sepolia will look like this: ... const config: HardhatUserConfig = { ... networks: { scrollSepolia: { url: 'https://sepolia-rpc.scroll.io' || '', accounts: process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], }, }, etherscan: { apiKey: { scrollSepolia: , }, customChains: [ { network: 'scrollSepolia', chainId: 534351, urls: { apiURL: 'https://api-sepolia.scrollscan.com/api', browserURL: 'https://sepolia.scrollscan.com/', }, }, ], },} ... Now you can verify the smart contract by running the following command. npx hardhat verify --network scrollSepolia For example, this is how a smart contract that receives two uint parameters in the constructor should look: npx hardhat verify --network scrollSepolia 0xD9880690bd717189cC3Fbe7B9020F27fae7Ac76F 123 456 Warning You may receive an error stating etherscan.apiKey.trim is not a function. If so, you need to update @nomiclabs/hardhat-etherscan to be able to support custom chains. Try running npm update @nomiclabs/hardhat-etherscan Foundry When using Foundry, the verify-contract command helps automate the process of verifying contracts. If your contract has constructor arguments, you can specify these in ABI-encoded form with the --constructor-args option. For example, if your constructor takes two uint256 variables: --constructor-args $(cast abi-encode "constructor(uint256,uint256)" 0 7) Refer to the Foundry documentation for further options you can specify. forge verify-contract \ --verifier-url https://api-sepolia.scrollscan.com/api \ --etherscan-api-key \ --constructor-args Caution Do not specify the chain ID. What's Next Scroll Contracts MoreEdit this pageJoin our community On This PageUsing Developer ToolsHardhatFoundry MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us ```plaintext ... const config: HardhatUserConfig = { ... networks: { scrollSepolia: { url: 'https://sepolia-rpc.scroll.io' || '', accounts: process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], }, }, etherscan: { apiKey: { scrollSepolia: , }, customChains: [ { network: 'scrollSepolia', chainId: 534351, urls: { apiURL: 'https://api-sepolia.scrollscan.com/api', browserURL: 'https://sepolia.scrollscan.com/', }, }, ], },} ... ``` ```plaintext npx hardhat verify --network scrollSepolia ``` ```plaintext npx hardhat verify --network scrollSepolia 0xD9880690bd717189cC3Fbe7B9020F27fae7Ac76F 123 456 ``` ```plaintext --constructor-args $(cast abi-encode "constructor(uint256,uint256)" 0 7) ``` ```plaintext forge verify-contract \ --verifier-url https://api-sepolia.scrollscan.com/api \ --etherscan-api-key \ --constructor-args ``` # Scroll Contracts | Scroll Documentation ## https://docs.scroll.io/en/developers/scroll-contracts Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer Scroll Contracts On This PageNetwork InfoScroll ContractsRollupETH and ERC20 BridgeAdvanced Bridge ContractsL2 PredeploysAdditional Useful ContractsTokensScroll Sepolia TestnetNetwork InfoScroll Sepolia ContractsAdditional Useful ContractsTokens In this article you’ll find useful contract addresses for Scroll and commonly used protocols. See the section below for Scroll Sepolia info. Network Info Use the table below to configure your Ethereum tools to the Scroll mainnet. Network NameScrollEthereum MainnetRPC URLhttps://rpc.scroll.io/https://eth.llamarpc.comChain ID5343521Currency SymbolETHETHBlock Explorer URLhttps://scrollscan.com/https://etherscan.io Additional Scroll Mainnet RPCs and Infra Scroll Native Bridge Scroll Rollup Scanner Scroll RPC Providers on ChainList.org Ethereum RPC Providers on ChainList.org Scroll Contracts Rollup L1 Rollup (Scroll Chain): 0xa13BAF47339d63B743e7Da8741db5456DAc1E556 ETH and ERC20 Bridge L1 Gateway Router: 0xF8B1378579659D8F7EE5f3C929c2f3E332E41Fd6 L2 Gateway Router: 0x4C0926FF5252A435FD19e10ED15e5a249Ba19d79 Advanced Bridge Contracts Scroll Messenger L1 Messenger: 0x6774Bcbd5ceCeF1336b5300fb5186a12DDD8b367 L2 Messenger: 0x781e90f1c8Fc4611c9b7497C3B47F99Ef6969CbC ETH Bridge L1 ETH Gateway: 0x7F2b8C31F88B6006c382775eea88297Ec1e3E905 L2 ETH Gateway: 0x6EA73e05AdC79974B931123675ea8F78FfdacDF0 L1 WETH Gateway: 0x7AC440cAe8EB6328de4fA621163a792c1EA9D4fE L2 WETH Gateway: 0x7003E7B7186f0E6601203b99F7B8DECBfA391cf9 ERC20 Bridge L1 ERC20 Standard Gateway: 0xD8A791fE2bE73eb6E6cF1eb0cb3F36adC9B3F8f9 L2 ERC20 Standard Gateway: 0xE2b4795039517653c5Ae8C2A9BFdd783b48f447A L1 ERC20 Custom Gateway: 0xb2b10a289A229415a124EFDeF310C10cb004B6ff L2 ERC20 Custom Gateway: 0x64CCBE37c9A82D85A1F2E74649b7A42923067988 ERC721 Bridge L1 ERC721 Gateway: 0x6260aF48e8948617b8FA17F4e5CEa2d21D21554B L2 ERC721 Gateway: 0x7bC08E1c04fb41d75F1410363F0c5746Eae80582 ERC1155 Bridge L1 ERC1155 Gateway: 0xb94f7F6ABcb811c5Ac709dE14E37590fcCd975B6 L2 ERC1155 Gateway: 0x62597Cc19703aF10B58feF87B0d5D29eFE263bcc Gas Oracle (deployed on Mainnet) L1 Message Queue With Gas Price Oracle (deprecated from Euclid): 0x0d7E906BD9cAFa154b048cFa766Cc1E54E39AF9B L1 Message Queue V2 With Gas Price Oracle: 0x56971da63A3C0205184FEF096E9ddFc7A8C2D18a L2 Predeploys Message Queue: 0x5300000000000000000000000000000000000000 Gas Price Oracle: 0x5300000000000000000000000000000000000002 Whitelist: 0x5300000000000000000000000000000000000003 WETH L2: 0x5300000000000000000000000000000000000004 Transaction Fee Vault: 0x5300000000000000000000000000000000000005 Additional Useful Contracts Multicall3: 0xcA11bde05977b3631167028862bE2a173976CA11 Tokens Bridged tokens from Ethereum Mainnet using the native bridge can be found on and added to the token-list repo. Scroll Sepolia Testnet Network Info Use the table below to configure your Ethereum tools to the Scroll Sepolia Testnet. Network NameScroll SepoliaEthereum SepoliaRPC URLhttps://sepolia-rpc.scroll.io/https://rpc2.sepolia.orgChain ID53435111155111Currency SymbolETHETHBlock Explorer URLhttps://sepolia.scrollscan.comhttps://sepolia.etherscan.io Additional Scroll Sepolia RPCs and Infra Scroll Sepolia Native Bridge Scroll Sepolia Rollup Scanner Scroll Sepolia RPC Providers on ChainList.org Ethereum Sepolia RPC Providers on ChainList.org Additional Block Explorers: Dora L2Scan Scroll Sepolia Contracts Rollup L1 Rollup (Scroll Chain): 0x2D567EcE699Eabe5afCd141eDB7A4f2D0D6ce8a0 ETH and ERC20 Bridge L1 Gateway Router: 0x13FBE0D0e5552b8c9c4AE9e2435F38f37355998a L2 Gateway Router: 0x9aD3c5617eCAa556d6E166787A97081907171230 Advanced Bridge Contracts Scroll Messenger L1 Messenger: 0x50c7d3e7f7c656493D1D76aaa1a836CedfCBB16A L2 Messenger: 0xBa50f5340FB9F3Bd074bD638c9BE13eCB36E603d ETH Bridge L1 ETH Gateway: 0x8A54A2347Da2562917304141ab67324615e9866d L2 ETH Gateway: 0x91e8ADDFe1358aCa5314c644312d38237fC1101C L1 WETH Gateway: 0x3dA0BF44814cfC678376b3311838272158211695 L2 WETH Gateway: 0x481B20A927206aF7A754dB8b904B052e2781ea27 ERC20 Bridge L1 ERC20 Standard Gateway: 0x65D123d6389b900d954677c26327bfc1C3e88A13 L2 ERC20 Standard Gateway: 0xaDcA915971A336EA2f5b567e662F5bd74AEf9582 L1 ERC20 Custom Gateway: 0x31C994F2017E71b82fd4D8118F140c81215bbb37 L2 ERC20 Custom Gateway: 0x058dec71E53079F9ED053F3a0bBca877F6f3eAcf ERC721 Bridge L1 ERC721 Gateway: 0xEF27A5E63aa3f1B8312f744b9b4DcEB910Ba77AC L2 ERC721 Gateway: 0x179B9415194B67DC3c0b8760E075cD4415785c97 ERC1155 Bridge L1 ERC1155 Gateway: 0xa5Df8530766A85936EE3E139dECE3bF081c83146 L2 ERC1155 Gateway: 0xe17C9b9C66FAF07753cdB04316D09f52144612A5 Gas Oracle (deployed on Sepolia) L1 Message Queue With Gas Price Oracle (deprecated from Euclid): 0xF0B2293F5D834eAe920c6974D50957A1732de763 L1 Message Queue V2 With Gas Price Oracle: 0xA0673eC0A48aa924f067F1274EcD281A10c5f19F L2 Gas Oracle (deprecated): 0x247969F4fad93a33d4826046bc3eAE0D36BdE548 L2 Predeploys Message Queue: 0x5300000000000000000000000000000000000000 Gas Price Oracle: 0x5300000000000000000000000000000000000002 Whitelist: 0x5300000000000000000000000000000000000003 WETH L2: 0x5300000000000000000000000000000000000004 Transaction Fee Vault: 0x5300000000000000000000000000000000000005 Additional Useful Contracts Multicall3: 0xcA11bde05977b3631167028862bE2a173976CA11 Tokens Bridged tokens from Ethereum Sepolia using the native bridge can be added to the token-list repo’s sepolia branch. Gho Token: 0xD9692f1748aFEe00FACE2da35242417dd05a8615 What's Next Ethereum & Scroll Differences MoreEdit this pageJoin our community On This PageNetwork InfoScroll ContractsRollupETH and ERC20 BridgeAdvanced Bridge ContractsL2 PredeploysAdditional Useful ContractsTokensScroll Sepolia TestnetNetwork InfoScroll Sepolia ContractsAdditional Useful ContractsTokens MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us | Network Name | Scroll | Ethereum Mainnet | || --- || --- || --- || | RPC URL | https://rpc.scroll.io/ | https://eth.llamarpc.com | | Chain ID | 534352 | 1 | | Currency Symbol | ETH | ETH | | Block Explorer URL | https://scrollscan.com/ | https://etherscan.io | | Network Name | Scroll Sepolia | Ethereum Sepolia | || --- || --- || --- || | RPC URL | https://sepolia-rpc.scroll.io/ | https://rpc2.sepolia.org | | Chain ID | 534351 | 11155111 | | Currency Symbol | ETH | ETH | | Block Explorer URL | https://sepolia.scrollscan.com | https://sepolia.etherscan.io | # Ethereum & Scroll Differences | Scroll Documentation ## https://docs.scroll.io/en/developers/ethereum-and-scroll-differences Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer Ethereum & Scroll Differences On This PageEVM OpcodesEVM PrecompilesPrecompile LimitsState AccountAdditional FieldsCodeHashCodeSizeBlock TimeTransaction OrderingReorgs and FinalityFuture EIPsTransaction FeesFootnotes A number of technical details differ between Ethereum mainnet’s EVM and Scroll’s modified design for a zkEVM. Below you can see those differences as they exist on Scroll and Scroll Sepolia. For open-source contributors and infrastructure builders, please contact our team for additional support. Don't Worry! For the average Solidity developer, these details won’t affect your development experience. EVM Opcodes OpcodeSolidity equivalentScroll BehaviorBLOCKHASHblock.blockhashReturns keccak(chain_id || block_number) for the last 256 blocks.COINBASEblock.coinbaseReturns the pre-deployed fee vault contract address. See Scroll Contracts.DIFFICULTY / PREVRANDAOblock.difficultyReturns 0.SELFDESTRUCTselfdestructDisabled. If the opcode is encountered, the transaction will be reverted.1 Opcodes related to EIP-4844, like BLOBHASH and BLOBBASEFEE, are not yet available on Scroll. Additionally, EIP-4788 for accessing the Beacon Chain block root is not supported. We support the cancun EVM target and the latest Solidity version 0.8.26. EVM Precompiles The RIPEMD-160 (address 0x3) blake2f (address 0x9), and point evaluation (address 0x0a) precompiles are currently not supported. Calls to unsupported precompiled contracts will revert. We plan to enable these precompiles in future hard forks. The modexp precompile is supported but only supports inputs of size less than or equal to 32 bytes (i.e. u256). The ecPairing precompile is supported, but the number of points(sets, pairs) is limited to 4, instead of 6. The other EVM precompiles are all supported: ecRecover, identity, ecAdd, ecMul. Precompile Limits Because of a bounded size of the zkEVM circuits, there is an upper limit on the number of calls that can be made for some precompiles. These transactions will not revert, but simply be skipped by the sequencer if they cannot fit into the space of the circuit. Read more about the Circuit Capacity Checker. Precompile / OpcodeLimitkeccak2563157ecRecover119modexp23ecAdd50ecMul50ecPairing2 State Account Additional Fields We added two fields in the current StateAccount object: PoseidonCodehash and CodeSize. type StateAccount struct { Nonce uint64 Balance *big.Int Root common.Hash // merkle root of the storage trie KeccakCodeHash []byte // still the Keccak codehash // added fields PoseidonCodeHash []byte // the Poseidon codehash CodeSize uint64} CodeHash Related to this, we maintain two types of codehash for each contract bytecode: Keccak hash and Poseidon hash. KeccakCodeHash is kept to maintain compatibility for EXTCODEHASH. PoseidonCodeHash is used for verifying the correctness of bytecodes loaded in the zkEVM, where Poseidon hashing is far more efficient. CodeSize When verifying EXTCODESIZE, it is expensive to load the whole contract data into the zkEVM. Instead, we store the contract size in storage during contract creation. This way, we do not need to load the code — a storage proof is sufficient to verify this opcode. Block Time To improve the throughput of the Scroll chain, we introduced a dynamic block time in our Curie upgrade. During the congestion period, a block will be sealed once the transactions in the block reach the circuit limit instead of waiting for the 3-second interval. During normal hours, blocks will still be sealed at 3-second interval to ensure a consistent user experience. Transaction Ordering Similarly to Ethereum, the Scroll sequencer aims to prioritize executable transactions based on their “tip” (priority fee). In most cases, transactions will be included in the next block in decreasing order of their tips. However, just like in Ethereum, this ordering is not guaranteed by the protocol, and some blocks might diverge from it. In particular, during periods of low mempool congestion, the sequencer will process transactions on a first-come-first-served basis, so some transactions might precede others with higher tip in the same block. Reorgs and Finality Starting from our DarwinV2 upgrade, the maximum reorg depth has been set to 17 blocks. Transaction ordering should be unchanged after this threshold, however the only absolute guarantee is for transactions to be finalized (proof submitted to L1). Future EIPs We keep a close eye on all emerging EIPs adopted by Ethereum and adopt them when suitable. If you’re interested in more specifics, reach out in our community forum or on the Scroll Discord. Transaction Fees The fee charged to Scroll transactions contains two parts: L2 gas fee: similar to L1, the amount of L2 execution fee equals to L2_gas_price * L2_gas_used, covering the following costs: L2 sequencer execution & storage cost Validity proof verification and finalization cost on L1 Prover cost L1 data fee: additional fee on top of L2 gas fee. The L1 data fee is only charged to L2-initiated transactions, not to L1-initiated transactions. The fee covers the cost of sending data to L1 for data availability. Because we roll up the tx data to L1, the L1 rollup fee is calculated based on the size of tx data. For more information, see Transaction Fees on Scroll. Footnotes Will change to adopt Ethereum’s solution in the future. ↩ What's Next L1 & L2 Bridging MoreEdit this pageJoin our community On This PageEVM OpcodesEVM PrecompilesPrecompile LimitsState AccountAdditional FieldsCodeHashCodeSizeBlock TimeTransaction OrderingReorgs and FinalityFuture EIPsTransaction FeesFootnotes MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us | Opcode | Solidity equivalent | Scroll Behavior | || --- || --- || --- || | BLOCKHASH | block.blockhash | Returns keccak(chain_id || block_number) for the last 256 blocks. | | COINBASE | block.coinbase | Returns the pre-deployed fee vault contract address. See Scroll Contracts. | | DIFFICULTY / PREVRANDAO | block.difficulty | Returns 0. | | SELFDESTRUCT | selfdestruct | Disabled. If the opcode is encountered, the transaction will be reverted.1 | | Precompile / Opcode | Limit | || --- || --- || | keccak256 | 3157 | | ecRecover | 119 | | modexp | 23 | | ecAdd | 50 | | ecMul | 50 | | ecPairing | 2 | ```plaintext type StateAccount struct { Nonce uint64 Balance *big.Int Root common.Hash // merkle root of the storage trie KeccakCodeHash []byte // still the Keccak codehash // added fields PoseidonCodeHash []byte // the Poseidon codehash CodeSize uint64} ``` # L1 and L2 Bridging | Scroll Documentation ## https://docs.scroll.io/en/developers/l1-and-l2-bridging Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer L1 and L2 Bridging On This PageL1 Gateway architectureL2 Gateway architecture The Scroll bridge enables the transfer of ETH, ERC20 tokens, NFTs, and arbitrary messages between L1 and L2. It serves as a secure mechanism for moving various digital assets across L1 and L2. To facilitate the transfer of ETH and ERC20 tokens, the Scroll bridge utilizes the Gateway Router. This contract ensures the smooth passage of these assets between L1 and L2, allowing users to transfer their Ethereum-based tokens seamlessly. The ERC721 and ERC1155 Gateway enables the transfer of non-fungible assets between the two networks, allowing users to move their NFTs across L1 and L2. In addition to token transfers, the Scroll Messenger contract enables cross-chain contract interaction. This means that contracts on one network can interact with contracts on the other network through the Scroll Messenger contract. This functionality expands the possibilities for decentralized applications and smart contracts to operate seamlessly across both networks. L1 Gateway architecture There are many entry points from the user to the Scroll bridge. This will depend on what you want to do and how you want to do it. If you want to send ETH or ERC20 tokens, you should use the GatewayRouter. If you want to send NFTs, you should use the L1ERC721Gateway or L1ERC1155Gateway. If you want to send arbitrary data, you should use the L1ScrollMessenger. All Gateway transfers use the Scroll Messenger to send assets cross-chain, whose job is to append the transactions to the Message Queue for L2 inclusion. L2 Gateway architecture Regarding possible permissionlessly callable entry points, the L2 Gateway Architecture is very similar to L1. The difference is that when sending a message from L2, calling the appendMessage function will store the message in an append-only binary merkle tree (aka withdraw tree) in the L2MessageQueue. When a new message is sent to the L2MessageQueue, the relayer will detect it and store it in the database. When the block is finalized, it will generate a proof of the new merkle path and pass it to the L1geth node to execute on L1ScrollMessenger. All finalized withdraw roots will be stored in the rollup contract so we can verify the proof against them. In the next Scroll versions, the Relayer won’t be needed since all users will be able to finalize the transaction on L1. In the upcoming sections, we will explore the technical aspects of the bridge, including the smart contract API required to utilize its capabilities. Detailed guides with code examples are provided in the Developer Guides section to assist developers and users in understanding and implementing these functionalities. What's Next ETH & ERC20 Token Bridge MoreEdit this pageJoin our community On This PageL1 Gateway architectureL2 Gateway architecture MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us # ETH and ERC20 Token Bridge | Scroll Documentation ## https://docs.scroll.io/en/developers/l1-and-l2-bridging/eth-and-erc20-token-bridge Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer ETH and ERC20 Token Bridge On This PageDeposit ETH and ERC20 tokens from L1Withdraw ETH and ERC20 tokens from L2Creating an ERC20 token with custom logic on L2Adding a Custom L2 ERC20 token to the Scroll BridgeL1 Gateway APIdepositETHdepositERC20getL2ERC20AddressupdateTokenMappingL2 Gateway APIwithdrawETHwithdrawERC20getL1ERC20AddressupdateTokenMapping Deposit ETH and ERC20 tokens from L1 The Gateway Router allows ETH and ERC20 token bridging from L1 to L2 using the depositETH and depositERC20 functions respectively. It is a permissionless bridge deployed on L1. Notice that ERC20 tokens will have a different address on L2, you can use the getL2ERC20Address function to query the new address. depositETH and depositERC20 are payable functions, the amount of ETH sent to these functions will be used to pay for L2 fees. If the amount is not enough, the transaction will not be sent. All excess ETH will be sent back to the sender. 0.00001 ETH should be more than enough to process a token deposit. When bridging ERC20 tokens, you don’t have to worry about selecting the right Gateway. This is because the L1GatewayRouter will choose the correct underlying entry point to send the message: L1StandardERC20Gateway: This Gateway permits any ERC20 deposit and will be selected as the default by the L1GatewayRouter for an ERC20 token that doesn’t need custom logic on L2. On the very first token bridging, a new token will be created on L2 that implements the ScrollStandardERC20. To bridge a token, call the depositERC20 function on the L1GatewayRouter. L1CustomERC20Gateway: This Gateway will be selected by the L1GatewayRouter for tokens with custom logic. For an L1/L2 token pair to work on the Scroll Custom ERC20 Bridge, the L2 token contract has to implement IScrollStandardERC20. Additionally, the token should grant mint or burn capability to the L2CustomERC20Gateway. Visit the Bridge an ERC20 through the Custom Gateway guide for a step-by-step example of how to bridge a custom token. All Gateway contracts will form the message and send it to the L1ScrollMessenger which can send arbitrary messages to L2. The L1ScrollMessenger passes the message to the L1MessageQueue. Any user can send messages directly to the Messenger to execute arbitrary data on L2. This means they can execute any function on L2 from a transaction made on L1 via the bridge. Although an application could directly pass messages to existing token contracts, the Gateway abstracts the specifics and simplifies making transfers and calls. In future upgrades, users will be able to bypass the L1ScrollMessenger and send messages directly to the L1MessageQueue. If a message is sent via the L1MessageQueue, the transaction’s sender will be the address of the user sending the transaction, not the address of the L1ScrollMessenger. When a new block gets created on L1, the Watcher will detect the message on the L1MessageQueue and will pass it to the Relayer service, which will submit the transaction to the L2 via the l2geth node. Finally, the l2geth node will pass the transaction to the L2ScrollMessenger contract for execution on L2. Withdraw ETH and ERC20 tokens from L2 The L2 Gateway is very similar to the L1 Gateway. We can withdraw ETH and ERC20 tokens back to L1 using the withdrawETH and withdrawERC20 functions. The contract address is deployed on L2. We use the getL1ERC20Address to retrieve the token address on L1. withdrawETH and withdrawERC20 are payable functions, and the amount of ETH sent to these functions will be used to pay for L1 fees. If the amount is not enough, the transaction will not be sent. All excess ETH will be sent back to the sender. Fees will depend on L1 activity but 0.005 ETH should be enough to process a token withdrawal. Make sure the transactions won’t revert on L1 while sending from L2. There is no way to recover bridged ETH, tokens, or NFTs if your transaction reverts on L1. All assets are burnt on Scroll when the transaction is sent, and it’s impossible to mint them again. Creating an ERC20 token with custom logic on L2 If a token needs custom logic on L2, it will need to be bridged through an L1CustomERC20Gateway and L2CustomERC20Gateway respectively. The custom token on L2 will need to give permission to the Gateway to mint new tokens when a deposit occurs and to burn when tokens are withdrawn The following interface is the IScrollStandardERC20 needed for deploying tokens compatible with the L2CustomERC20Gateway on L2. interface IScrollStandardERC20 { /// @notice Return the address of Gateway the token belongs to. function gateway() external view returns (address); /// @notice Return the address of counterpart token. function counterpart() external view returns (address); /// @dev ERC677 Standard, see https://github.com/ethereum/EIPs/issues/677 /// Defi can use this method to transfer L1/L2 token to L2/L1, /// and deposit to L2/L1 contract in one transaction function transferAndCall(address receiver, uint256 amount, bytes calldata data) external returns (bool success); /// @notice Mint some token to recipient's account. /// @dev Gateway Utilities, only gateway contract can call /// @param _to The address of recipient. /// @param _amount The amount of token to mint. function mint(address _to, uint256 _amount) external; /// @notice Burn some token from account. /// @dev Gateway Utilities, only gateway contract can call /// @param _from The address of account to burn token. /// @param _amount The amount of token to mint. function burn(address _from, uint256 _amount) external;} Adding a Custom L2 ERC20 token to the Scroll Bridge Tokens can be bridged securely and permissionlessly through Gateway contracts deployed by any developer. However, Scroll also manages an ERC20 Router and a Gateway where all tokens created by the community are welcome. Being part of the Scroll-managed Gateway means you won’t need to deploy the Gateway contracts, and your token will appear in the Scroll frontend. To be part of the Scroll Gateway, you must contact the Scroll team to add the token to both L1 and L2 bridge contracts. To do so, follow the instructions on the token lists repository to add your new token to the official Scroll frontend. L1 Gateway API Please visit the npm library for the complete Scroll contract API documentation. depositETH function depositETH(address _to, uint256 _amount, uint256 _gasLimit) public payable; Sends ETH from L1 to L2. ParameterDescriptiontoThe address of recipient’s account on L2.amountThe amount of token to transfer, in wei.gasLimitGas limit required to complete the deposit on L2. 170000 should be enough to process the transaction, but unused funds are refunded. depositERC20 function depositERC20(address _token, address _to, uint256 _amount, uint256 _gasLimit) payable; Sends ERC20 tokens from L1 to L2. ParameterDescriptiontokenThe token address on L1.toThe address of recipient’s account on L2.amountThe amount of token to transfer, in wei.gasLimitGas limit required to complete the deposit on L2. 200000 should be enough to process the transaction, depending on the Gateway, but unused funds are refunded. getL2ERC20Address function getL2ERC20Address(address _l1Token) external view returns (address); Returns the corresponding L2 token address given L1 token address. ParameterDescription_l1TokenThe address of l1 token. updateTokenMapping function updateTokenMapping(address _l1Token, address _l2Token) external; Update the mapping that connects an ERC20 token from L1 to L2. ParameterDescription_l1TokenThe address of the ERC20 token in L1._l2TokenThe address of corresponding ERC20 token in L2. L2 Gateway API withdrawETH function withdrawETH(address to, uint256 amount, uint256 gasLimit) external payable; Sends ETH from L2 to L1. ParameterDescriptiontoThe address of recipient’s account on L1.amountThe amount of token to transfer, in wei.gasLimitGas limit required to complete the deposit on L1. This is optional, send 0 if you don’t want to set it. withdrawERC20 function withdrawERC20(address token, address to, uint256 amount, uint256 gasLimit) external payable; Sends ERC20 tokens from L2 to L1. ParameterDescriptiontokenThe token address on L2.toThe address of recipient’s account on L1.amountThe amount of token to transfer, in wei.gasLimitGas limit required to complete the deposit on L1. This is optional, send 0 if you don’t want to set it. getL1ERC20Address function getL1ERC20Address(address l2Token) external view returns (address); Returns the corresponding L1 token address given an L2 token address. ParameterDescriptionl2TokenThe address of the L2 token. updateTokenMapping function updateTokenMapping(address _l1Token, address _l2Token) external; Update the mapping that connects an ERC20 contract from L2 to L1. ParameterDescription_l2TokenThe address of the ERC20 token in L2._l1TokenThe address of corresponding ERC20 token in L1. What's Next ERC721 NFT Bridge MoreEdit this pageJoin our community On This PageDeposit ETH and ERC20 tokens from L1Withdraw ETH and ERC20 tokens from L2Creating an ERC20 token with custom logic on L2Adding a Custom L2 ERC20 token to the Scroll BridgeL1 Gateway APIdepositETHdepositERC20getL2ERC20AddressupdateTokenMappingL2 Gateway APIwithdrawETHwithdrawERC20getL1ERC20AddressupdateTokenMapping MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us | Parameter | Description | || --- || --- || | to | The address of recipient’s account on L2. | | amount | The amount of token to transfer, in wei. | | gasLimit | Gas limit required to complete the deposit on L2. 170000 should be enough to process the transaction, but unused funds are refunded. | | Parameter | Description | || --- || --- || | token | The token address on L1. | | to | The address of recipient’s account on L2. | | amount | The amount of token to transfer, in wei. | | gasLimit | Gas limit required to complete the deposit on L2. 200000 should be enough to process the transaction, depending on the Gateway, but unused funds are refunded. | | Parameter | Description | || --- || --- || | _l1Token | The address of l1 token. | | Parameter | Description | || --- || --- || | _l1Token | The address of the ERC20 token in L1. | | _l2Token | The address of corresponding ERC20 token in L2. | | Parameter | Description | || --- || --- || | to | The address of recipient’s account on L1. | | amount | The amount of token to transfer, in wei. | | gasLimit | Gas limit required to complete the deposit on L1. This is optional, send 0 if you don’t want to set it. | | Parameter | Description | || --- || --- || | token | The token address on L2. | | to | The address of recipient’s account on L1. | | amount | The amount of token to transfer, in wei. | | gasLimit | Gas limit required to complete the deposit on L1. This is optional, send 0 if you don’t want to set it. | | Parameter | Description | || --- || --- || | l2Token | The address of the L2 token. | | Parameter | Description | || --- || --- || | _l2Token | The address of the ERC20 token in L2. | | _l1Token | The address of corresponding ERC20 token in L1. | ```plaintext interface IScrollStandardERC20 { /// @notice Return the address of Gateway the token belongs to. function gateway() external view returns (address); /// @notice Return the address of counterpart token. function counterpart() external view returns (address); /// @dev ERC677 Standard, see https://github.com/ethereum/EIPs/issues/677 /// Defi can use this method to transfer L1/L2 token to L2/L1, /// and deposit to L2/L1 contract in one transaction function transferAndCall(address receiver, uint256 amount, bytes calldata data) external returns (bool success); /// @notice Mint some token to recipient's account. /// @dev Gateway Utilities, only gateway contract can call /// @param _to The address of recipient. /// @param _amount The amount of token to mint. function mint(address _to, uint256 _amount) external; /// @notice Burn some token from account. /// @dev Gateway Utilities, only gateway contract can call /// @param _from The address of account to burn token. /// @param _amount The amount of token to mint. function burn(address _from, uint256 _amount) external;} ``` ```plaintext function depositETH(address _to, uint256 _amount, uint256 _gasLimit) public payable; ``` ```plaintext function depositERC20(address _token, address _to, uint256 _amount, uint256 _gasLimit) payable; ``` ```plaintext function getL2ERC20Address(address _l1Token) external view returns (address); ``` ```plaintext function updateTokenMapping(address _l1Token, address _l2Token) external; ``` ```plaintext function withdrawETH(address to, uint256 amount, uint256 gasLimit) external payable; ``` ```plaintext function withdrawERC20(address token, address to, uint256 amount, uint256 gasLimit) external payable; ``` ```plaintext function getL1ERC20Address(address l2Token) external view returns (address); ``` ```plaintext function updateTokenMapping(address _l1Token, address _l2Token) external; ``` # ERC721 NFT Bridge | Scroll Documentation ## https://docs.scroll.io/en/developers/l1-and-l2-bridging/erc721-nft-bridge Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer ERC721 NFT Bridge On This PageDeposit ERC721 tokens from L1Creating a ScrollERC721 token on L2Adding ERC721 NFTs to the Scroll BridgeWithdraw ERC721 tokens from ScrollL1ERC721Gateway APIdepositERC721updateTokenMappingL2ERC721Gateway APIwithdrawERC721updateTokenMapping Deposit ERC721 tokens from L1 NFT bridging from L1 to L2 is done via the L1ERC721Gateway contract instead of using a router. Similarly to bridging ERC20 tokens, we use the depositERC721 function to send tokens to L2, and we can later retrieve them back to L1 using withdrawERC721 on the L2ERC721Gateway contract deployed on L2. NFT contracts on both L1 and l2 must be launched and connected through the Gateways to enable bridging. This means deposit and withdraw transactions will revert if a contract on either L1 or L2 is missing or not mapped through the updateTokenMapping function. depositERC721 is a payable function. The amount of ETH sent to this function will be used to pay for L2 fees. If the amount is not enough, the transaction will not be sent. All excess eth will be sent back to the sender. 0.00001 ETH should be more than enough to process a token deposit. Creating a ScrollERC721 token on L2 To deposit an ERC721 token to L2, a token contract compatible with the IScrollERC721 standard has to be launched and mapped on a L1ERC721Gateway and L2ERC721Gateway on both L1 and L2, respectively. This contract has to grant permission to the Gateway on L2 to mint when a token is deposited and burn when the token is withdrawn. The following interface is the IScrollERC721 needed for deploying ERC721 tokens compatible with the L2ERC721Gateway on L2. interface IScrollERC721 { /// @notice Return the address of Gateway the token belongs to. function gateway() external view returns (address); /// @notice Return the address of counterpart token. function counterpart() external view returns (address); /// @notice Mint some token to recipient's account. /// @dev Gateway Utilities, only gateway contract can call /// @param _to The address of recipient. /// @param _tokenId The token id to mint. function mint(address _to, uint256 _tokenId) external; /// @notice Burn some token from account. /// @dev Gateway Utilities, only gateway contract can call /// @param _tokenId The token id to burn. function burn(uint256 _tokenId) external;} Adding ERC721 NFTs to the Scroll Bridge All assets can be bridged securely and permissionlessly through Gateway contracts deployed by any developer. However, Scroll also manages an ERC721 Gateway contract where all NFTs created by the community are welcome. Being part of the Scroll-managed Gateway means you won’t need to deploy the Gateway contracts, and your token will appear in the Scroll frontend. To be part of the Scroll Gateway, you must contact the Scroll team to add the NFT on both L1 and L2 Gateway contracts. To do so, follow the instructions on the token lists repository to add a new token to the Scroll official frontend. Withdraw ERC721 tokens from Scroll The L2ERC721Gateway contract is used to send tokens from L2 to L1. Before bridging, the L2ERC721Gateway contract has to be approved by the NFT contract. Once that is done, withdrawERC721 can be called to perform the asset bridge. withdrawERC721 is a payable function, and the amount of ETH sent to this function will be used to pay for L1 fees. If the amount is not enough, the transaction will not be sent. All excess ETH will be sent back to sender. Fees depend on L1 activity but 0.005 ETH should be enough to process a token withdrawal. Make sure the transaction won’t revert on L1 while sending from L2. There is no way to recover the NFT bridged if your transaction reverts on L1. All assets are burnt on L2 when the transaction is sent, and it’s impossible to mint them again. L1ERC721Gateway API Please visit the npm library for the complete Scroll contract API documentation. depositERC721 function depositERC721(address _token, address _to, uint256 _tokenId, uint256 _gasLimit) external payable; Deposit an ERC721 NFT from L1 to a recipient’s account on L2. ParameterDescriptiontokenThe address of ERC721 NFT contract on L1.toThe address of recipient’s account on L2.tokenIdThe NFT id to deposit.gasLimitGas limit required to complete the deposit on L2. Unused portion of fee will be refunded. updateTokenMapping function updateTokenMapping(address _l1Token, address _l2Token) external; Update the mapping that connects an NFT contract from L1 to L2. ParameterDescription_l1TokenThe address of ERC721 token in L1._l2TokenThe address of corresponding ERC721 token in L2. L2ERC721Gateway API withdrawERC721 function withdrawERC721(address _token, address _to, uint256 _tokenId, uint256 _gasLimit) external payable; Send an ERC721 NFT from L2 to a recipient’s account on L1. ParameterDescriptiontokenThe address of ERC721 NFT token contract on L2.toThe address of recipient’s account on L1.tokenIdThe NFT id to deposit.gasLimitUnused, but included for potential forward compatibility considerations. updateTokenMapping function updateTokenMapping(address _l1Token, address _l2Token) external; Update the mapping that connects an NFT contract from L2 to L1. ParameterDescription_l2TokenThe address of ERC721 token in L2._l1TokenThe address of corresponding ERC721 token in L1. What's Next ERC1155 Token Bridge MoreEdit this pageJoin our community On This PageDeposit ERC721 tokens from L1Creating a ScrollERC721 token on L2Adding ERC721 NFTs to the Scroll BridgeWithdraw ERC721 tokens from ScrollL1ERC721Gateway APIdepositERC721updateTokenMappingL2ERC721Gateway APIwithdrawERC721updateTokenMapping MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us | Parameter | Description | || --- || --- || | token | The address of ERC721 NFT contract on L1. | | to | The address of recipient’s account on L2. | | tokenId | The NFT id to deposit. | | gasLimit | Gas limit required to complete the deposit on L2. Unused portion of fee will be refunded. | | Parameter | Description | || --- || --- || | _l1Token | The address of ERC721 token in L1. | | _l2Token | The address of corresponding ERC721 token in L2. | | Parameter | Description | || --- || --- || | token | The address of ERC721 NFT token contract on L2. | | to | The address of recipient’s account on L1. | | tokenId | The NFT id to deposit. | | gasLimit | Unused, but included for potential forward compatibility considerations. | | Parameter | Description | || --- || --- || | _l2Token | The address of ERC721 token in L2. | | _l1Token | The address of corresponding ERC721 token in L1. | ```plaintext interface IScrollERC721 { /// @notice Return the address of Gateway the token belongs to. function gateway() external view returns (address); /// @notice Return the address of counterpart token. function counterpart() external view returns (address); /// @notice Mint some token to recipient's account. /// @dev Gateway Utilities, only gateway contract can call /// @param _to The address of recipient. /// @param _tokenId The token id to mint. function mint(address _to, uint256 _tokenId) external; /// @notice Burn some token from account. /// @dev Gateway Utilities, only gateway contract can call /// @param _tokenId The token id to burn. function burn(uint256 _tokenId) external;} ``` ```plaintext function depositERC721(address _token, address _to, uint256 _tokenId, uint256 _gasLimit) external payable; ``` ```plaintext function updateTokenMapping(address _l1Token, address _l2Token) external; ``` ```plaintext function withdrawERC721(address _token, address _to, uint256 _tokenId, uint256 _gasLimit) external payable; ``` ```plaintext function updateTokenMapping(address _l1Token, address _l2Token) external; ``` # ERC1155 Token Bridge | Scroll Documentation ## https://docs.scroll.io/en/developers/l1-and-l2-bridging/erc1155-token-bridge Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer ERC1155 Token Bridge On This PageDeposit ERC1155 tokens from L1Creating an ERC1155 token on L2Adding an ERC1155 token to the Scroll BridgeWithdraw ERC1155 tokens from L2L1ERC1155Gateway APIdepositERC1155updateTokenMappingL2ERC1155Gateway APIwithdrawERC1155updateTokenMapping Deposit ERC1155 tokens from L1 ERC1155 bridging from L1 to L2 is done via the L1ERC1155Gateway. Similarly to ERC721 bridging, we don’t use a router but the depositERC1155 function on the Gateway directly. depositERC1155 is a payable function, and the amount of ETH sent to this function will be used to pay for L2 fees. If the amount is not enough, the transaction will not be sent. All excess eth will be sent back to the sender. 0.00001 ETH should be more than enough to process a token deposit. Creating an ERC1155 token on L2 Similar to ERC721 bridging, in order to bridge ERC1155 tokens, a contract compatible with the IScrollERC1155 standard has to be launched and mapped on a L1ERC1155Gateway and L2ERC1155Gateway on both L1 and L2 respectively. This contract has to grant permission to the Gateway on L2 to mint when a token is deposited and burn when the token is withdrawn. The following interface is the IScrollERC1155 needed for deploying ERC1155 tokens compatible with the L2ERC1155Gateway on L2. interface IScrollERC1155 { /// @notice Return the address of Gateway the token belongs to. function gateway() external view returns (address); /// @notice Return the address of counterpart token. function counterpart() external view returns (address); /// @notice Mint some token to recipient's account. /// @dev Gateway Utilities, only gateway contract can call /// @param _to The address of recipient. /// @param _tokenId The token id to mint. /// @param _amount The amount of token to mint. /// @param _data The data passed to recipient function mint(address _to, uint256 _tokenId, uint256 _amount, bytes memory _data) external; /// @notice Burn some token from account. /// @dev Gateway Utilities, only gateway contract can call /// @param _from The address of account to burn token. /// @param _tokenId The token id to burn. /// @param _amount The amount of token to burn. function burn(address _from, uint256 _tokenId, uint256 _amount) external; /// @notice Batch mint some token to recipient's account. /// @dev Gateway Utilities, only gateway contract can call /// @param _to The address of recipient. /// @param _tokenIds The list of token ids to mint. /// @param _amounts The list of corresponding amount of token to mint. /// @param _data The data passed to recipient function batchMint( address _to, uint256[] calldata _tokenIds, uint256[] calldata _amounts, bytes calldata _data ) external; /// @notice Batch burn some token from account. /// @dev Gateway Utilities, only gateway contract can call /// @param _from The address of account to burn token. /// @param _tokenIds The list of token ids to burn. /// @param _amounts The list of corresponding amount of token to burn. function batchBurn(address _from, uint256[] calldata _tokenIds, uint256[] calldata _amounts) external;} Adding an ERC1155 token to the Scroll Bridge All assets can be bridged securely and permissionlessly through Gateway contracts deployed by any developer. However, Scroll also manages an ERC1155 Gateway contract where all NFTs created by the community are welcome. Being part of the Scroll-managed Gateway means you won’t need to deploy the Gateway contracts, and your token will appear in the Scroll frontend. To be part of the Scroll Gateway, you must contact the Scroll team to add the token to both L1 and L2 Gateway contracts. To do so, follow the instructions on the token lists repository to add your token to the Scroll official frontend. Withdraw ERC1155 tokens from L2 The L2ERC1155Gateway contract is used to send tokens from L2 to L1. Before bridging, the L2ERC1155Gateway contract has to be approved by the token contract. Once that is done, withdrawERC1155 can be called to perform the asset bridge. withdrawERC1155 is a payable function, and the amount of ETH sent to this function will be used to pay for L1 fees. If the amount is not enough, the transaction will not be sent. All excess ETH will be sent back to sender. Fees depend on L1 activity but 0.005 ETH should be enough to process a token withdrawal. Make sure the transaction won’t revert on L1 while sending from L2. There is no way to recover tokens bridged if you’re transaction reverts on L1. All assets are burnt on L2 when the transaction is sent, and it’s impossible to mint them again. L1ERC1155Gateway API Please visit the npm library for the complete Scroll contract API documentation. depositERC1155 function depositERC1155( address _token, address _to, uint256 _tokenId, uint256 _amount, uint256 _gasLimit) external payable; Deposit an ERC1155 token from L1 to a recipient’s account on L2. ParameterDescriptiontokenThe address of ERC1155 token contract on L1.toThe address of recipient’s account on L2.tokenIdThe NFT id to deposit.amountThe amount of tokens to deposit.gasLimitGas limit required to complete the deposit on L2. Unused portion of fee will be refunded. updateTokenMapping function updateTokenMapping(address _l1Token, address _l2Token) external; Update the mapping that connects an ERC1155 token contract from L1 to L2. ParameterDescription_l1TokenThe address of the ERC1155 token in L1._l2TokenThe address of corresponding ERC1155 token in L2. L2ERC1155Gateway API withdrawERC1155 function withdrawERC1155(address token, address to, uint256 tokenId, uint256 amount, uint256 gasLimit) external payable; Send ERC1155 tokens from L2 to a recipient’s account on L1. ParameterDescriptiontokenThe address of ERC1155 token contract on L2.toThe address of recipient’s account on L1.tokenIdThe NFT id to withdraw.amountThe amount of tokens to withdraw.gasLimitUnused, but included for potential forward compatibility considerations. updateTokenMapping function updateTokenMapping(address _l1Token, address _l2Token) external; Update the mapping that connects an ERC1155 token contract from L2 to L1. ParameterDescription_l1TokenThe address of the ERC1155 token in L1._l2TokenThe address of corresponding ERC1155 token in L2. What's Next The Scroll Messenger MoreEdit this pageJoin our community On This PageDeposit ERC1155 tokens from L1Creating an ERC1155 token on L2Adding an ERC1155 token to the Scroll BridgeWithdraw ERC1155 tokens from L2L1ERC1155Gateway APIdepositERC1155updateTokenMappingL2ERC1155Gateway APIwithdrawERC1155updateTokenMapping MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us | Parameter | Description | || --- || --- || | token | The address of ERC1155 token contract on L1. | | to | The address of recipient’s account on L2. | | tokenId | The NFT id to deposit. | | amount | The amount of tokens to deposit. | | gasLimit | Gas limit required to complete the deposit on L2. Unused portion of fee will be refunded. | | Parameter | Description | || --- || --- || | _l1Token | The address of the ERC1155 token in L1. | | _l2Token | The address of corresponding ERC1155 token in L2. | | Parameter | Description | || --- || --- || | token | The address of ERC1155 token contract on L2. | | to | The address of recipient’s account on L1. | | tokenId | The NFT id to withdraw. | | amount | The amount of tokens to withdraw. | | gasLimit | Unused, but included for potential forward compatibility considerations. | | Parameter | Description | || --- || --- || | _l1Token | The address of the ERC1155 token in L1. | | _l2Token | The address of corresponding ERC1155 token in L2. | ```plaintext interface IScrollERC1155 { /// @notice Return the address of Gateway the token belongs to. function gateway() external view returns (address); /// @notice Return the address of counterpart token. function counterpart() external view returns (address); /// @notice Mint some token to recipient's account. /// @dev Gateway Utilities, only gateway contract can call /// @param _to The address of recipient. /// @param _tokenId The token id to mint. /// @param _amount The amount of token to mint. /// @param _data The data passed to recipient function mint(address _to, uint256 _tokenId, uint256 _amount, bytes memory _data) external; /// @notice Burn some token from account. /// @dev Gateway Utilities, only gateway contract can call /// @param _from The address of account to burn token. /// @param _tokenId The token id to burn. /// @param _amount The amount of token to burn. function burn(address _from, uint256 _tokenId, uint256 _amount) external; /// @notice Batch mint some token to recipient's account. /// @dev Gateway Utilities, only gateway contract can call /// @param _to The address of recipient. /// @param _tokenIds The list of token ids to mint. /// @param _amounts The list of corresponding amount of token to mint. /// @param _data The data passed to recipient function batchMint( address _to, uint256[] calldata _tokenIds, uint256[] calldata _amounts, bytes calldata _data ) external; /// @notice Batch burn some token from account. /// @dev Gateway Utilities, only gateway contract can call /// @param _from The address of account to burn token. /// @param _tokenIds The list of token ids to burn. /// @param _amounts The list of corresponding amount of token to burn. function batchBurn(address _from, uint256[] calldata _tokenIds, uint256[] calldata _amounts) external;} ``` ```plaintext function depositERC1155( address _token, address _to, uint256 _tokenId, uint256 _amount, uint256 _gasLimit) external payable; ``` ```plaintext function updateTokenMapping(address _l1Token, address _l2Token) external; ``` ```plaintext function withdrawERC1155(address token, address to, uint256 tokenId, uint256 amount, uint256 gasLimit) external payable; ``` ```plaintext function updateTokenMapping(address _l1Token, address _l2Token) external; ``` # The Scroll Messenger | Scroll Documentation ## https://docs.scroll.io/en/developers/l1-and-l2-bridging/the-scroll-messenger Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer The Scroll Messenger On This PageFinalizing transactions on L1Messenger APIsendMessagerelayMessageWithProof The Scroll Messenger contracts allow for sending arbitrary messages from L1 to L2 or vice versa. This enables executing functions on another chain in a secure and permissionless way. To send a message from L1 to L2, use the messenger smart contract deployed on L1, L1ScrollMessenger. To send a message from L2 to L1, use the contract deployed on L2, L2ScrollMessenger. When sending a transaction through the Scroll Messenger deployed on L1 and L2, the resulting transaction sender (CALLER or msg.sender) will be the Messenger Contract address deployed on the receiving chain.In future Scroll versions, enforced transactions from the L1 will allow setting the sender on L2 as the original EOA on L1. It will also allow 3rd parties to relay signed transactions securely. Finalizing transactions on L1 Any upcoming transactions from L2 need to be finalized using the relayMessageWithProof function on the Scroll Messenger contract. We call this process “submitting an Execute Withdrawal transaction,” and it is required for both sending arbitrary messages and transferring assets through a gateway or the router. When you use relayMessageWithProof, you’ll have to provide a Merkle inclusion proof showing your transaction is included in the trie of “withdrawal” messages, along with other parameters. Producing this proof and these values can be done locally and permissionlessly, but at the moment, the easiest way to retrieve these parameters is through our backend APIs: Scroll Sepolia API: https://sepolia-api-bridge-v2.scroll.io/api/ Scroll API: https://mainnet-api-bridge-v2.scroll.io/api/ Experimental API This API was made for our Bridge UI. It is not yet finalized and may change in the future. We will update this guide when the API is finalized. Additionally, all examples below use the Sepolia API service — the calls should be easily adapted to work on mainnet. Supply the address of the EOA or contract responsible for initiating the original transaction on L2 to the /claimable endpoint. The API backend will provide you with all the necessary information to successfully conclude the transaction on L1. Take a look at the following example: https://sepolia-api-bridge.scroll.io/api/claimable?address=0x031407eaaffFB4f1EC2c999bE4477E52C81de3c5&page_size=10&page=1 The API should return your transaction data in the following format: { "errcode": 0, "errmsg": "", "data": { "result": [ { "hash": "0xa476850306d6ee52b127628ded34dcf2343570873cce9c5383bd497db48d4f9b", "amount": "", "to": "", "isL1": false, "l1Token": "", "l2Token": "", "blockNumber": 748, "blockTimestamp": null, "finalizeTx": { "hash": "", "amount": "", "to": "", "isL1": false, "blockNumber": 0, "blockTimestamp": null }, "claimInfo": { "from": "0x031407eaaffFB4f1EC2c999bE4477E52C81de3c5", "to": "0x1039057185CFe192d16c03F5656225821A193FD5", "value": "0", "nonce": "9", "batch_hash": "0x49a18d72dbceeb957f918947b532db452c031f528e7e6bf329007066638c5e50", "message": "0xa413686200000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000005686f6c6973000000000000000000000000000000000000000000000000000000", "proof": "0x69b4ee6cf9a38bed79668ddd347fef2bdff44c3760c9309fa41decfd60202d22ad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5b4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d3079f53171df5c0661d2afe86c4d97b6f34278daf6a0ea9baff5b4fc979d5629a5", "batch_index": "93" }, "createdTime": null } ], "total": 1 }} The claimInfo object under the result json returned has all the information needed to execute your transaction on L1. The parameters needed by the relayMessageWithProof are: from, to, value, nonce, message and proof. Supply these to the relayMessageWithProof function on L1 to execute and finalize your transaction on L1. All L2 transactions are bundled into batches – you have to wait for the batch that includes your transaction to finalize before calling relayMessageWithProof. Your transaction batch index is returned in the batch_index value on the /claimable endpoint, and you can follow the progress on the Scroll Rollup Explorer. Messenger API Please visit the npm library for the complete Scroll contract API documentation. sendMessage function sendMessage( address target, uint256 value, bytes calldata message, uint256 gasLimit, address refundAddress) external payable; Sends arbitrary data from one chain to another. It allows us to execute functions cross-chain. ParameterDescriptiontargetThe address of the account that receives the message. The receiver can be either a smart contract or an EOA wallet.valueThe amount of ether passed when calling the target contract.messageThe content of the message. This is the arbitrary calldata to be executed.gasLimitGas limit required to complete the message relay on the corresponding chain.refundAddressThe address of the account that will receive the refunded fee. relayMessageWithProof function relayMessageWithProof( address from, address to, uint256 value, uint256 nonce, bytes memory message, L2MessageProof memory proof) external; Relay a L2 => L1 message with message proof. ParameterDescriptionfromThe address of the sender of the message.toThe address of the recipient of the message.valueThe msg.value passed to the message call.nonceThe nonce of the message to avoid replay attack.messageThe content of the message.proofThe proof used to verify the correctness of the transaction. What's Next Transaction Fees on Scroll MoreEdit this pageJoin our community On This PageFinalizing transactions on L1Messenger APIsendMessagerelayMessageWithProof MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us | Parameter | Description | || --- || --- || | target | The address of the account that receives the message. The receiver can be either a smart contract or an EOA wallet. | | value | The amount of ether passed when calling the target contract. | | message | The content of the message. This is the arbitrary calldata to be executed. | | gasLimit | Gas limit required to complete the message relay on the corresponding chain. | | refundAddress | The address of the account that will receive the refunded fee. | | Parameter | Description | || --- || --- || | from | The address of the sender of the message. | | to | The address of the recipient of the message. | | value | The msg.value passed to the message call. | | nonce | The nonce of the message to avoid replay attack. | | message | The content of the message. | | proof | The proof used to verify the correctness of the transaction. | ```plaintext https://sepolia-api-bridge.scroll.io/api/claimable?address=0x031407eaaffFB4f1EC2c999bE4477E52C81de3c5&page_size=10&page=1 ``` ```plaintext { "errcode": 0, "errmsg": "", "data": { "result": [ { "hash": "0xa476850306d6ee52b127628ded34dcf2343570873cce9c5383bd497db48d4f9b", "amount": "", "to": "", "isL1": false, "l1Token": "", "l2Token": "", "blockNumber": 748, "blockTimestamp": null, "finalizeTx": { "hash": "", "amount": "", "to": "", "isL1": false, "blockNumber": 0, "blockTimestamp": null }, "claimInfo": { "from": "0x031407eaaffFB4f1EC2c999bE4477E52C81de3c5", "to": "0x1039057185CFe192d16c03F5656225821A193FD5", "value": "0", "nonce": "9", "batch_hash": "0x49a18d72dbceeb957f918947b532db452c031f528e7e6bf329007066638c5e50", "message": "0xa413686200000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000005686f6c6973000000000000000000000000000000000000000000000000000000", "proof": "0x69b4ee6cf9a38bed79668ddd347fef2bdff44c3760c9309fa41decfd60202d22ad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5b4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d3079f53171df5c0661d2afe86c4d97b6f34278daf6a0ea9baff5b4fc979d5629a5", "batch_index": "93" }, "createdTime": null } ], "total": 1 }} ``` ```plaintext function sendMessage( address target, uint256 value, bytes calldata message, uint256 gasLimit, address refundAddress) external payable; ``` ```plaintext function relayMessageWithProof( address from, address to, uint256 value, uint256 nonce, bytes memory message, L2MessageProof memory proof) external; ``` # Transaction Fees on Scroll | Scroll Documentation ## https://docs.scroll.io/en/developers/transaction-fees-on-scroll Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer Transaction Fees on Scroll On This PageOverviewL2 FeeCalculating the Execution FeeL1 FeeEstimating the L1 Data FeeCalculating the L1 Data Fee with Gas OracleL1GasPriceOracle APIL1 Originated TransactionsFuture RoadmapFootnotes Overview Scroll fees are notably lower than on its supporting layer. For users and developers, transaction fees on Scroll appear to work similarly to those on Ethereum mainnet, and existing tools, wallets, and code will likely work as if they were. However, the transaction fee shown in the wallet isn’t the whole picture unless the software is specifically aware of Scroll’s fee calculations. Due to the design of L2 rollups, transaction costs depend on the L1’s costs. To leverage Ethereum’s security, Scroll must account for the cost of the transaction data and proofs that must be stored and verified on the L1. Compared to Ethereum mainnet, Scroll introduces some new dimensions to transaction fee calculation to do this. The final cost of a transaction is constructed from several parts: L2 fee Calculated in the same manner as on the L1, equaling gas_price * gas_used L1 fee This additional fee covers sending data to L1 for data availability. Transactions initiated on the L1 do not pay this fee. It is calculated based on the size of the transaction’s calldata ETH is automatically deducted from the user’s balance on Scroll for the fee At a high level, the L2 fee is the cost of executing your transaction on the L2 sequencer, and the L1 fee is the cost of committing that transaction onto L1. In short, totalTxFee = l2Fee + l1Fee, all denominated in ETH, the native gas token for the Scroll network. Where are transaction fees sent? All tx fees are collected into the L2ScrollFeeVault contract balance. This contract also tracks the amount we’ve historically withdrawn to L1 using totalProcessed()(uint256).The block producer receives no direct reward, and the COINBASE opcode returns the fee vault address. L2 Fee Transactions on Scroll, like on Ethereum, must pay the cost of executing their computations and storing the data they produce. Calculating the Execution Fee The L2 execution fee is straightforward: l2TransactionExecutionFee = l2TransactionGasUsed * l2TransactionGasPrice The total fee depends on what the transaction does (l2TransactionGasUsed) as well as the current market conditions (l2TransactionGasPrice). Users set the gas price, and the “gas used” is assessed by calling the estimateGas endpoint on a Scroll node. In other words, the execution fee is calculated precisely like pre-EIP1559 Ethereum. L1 Fee Every transaction’s calldata must be committed to Ethereum, which incurs an additional transaction fee, referred to as the “L1 Fee”. Without doing this, Scroll couldn’t be reconstructed from only L1 data. Transactions aren’t committed 1-by-1 — they are collected in batches of blocks (and blocks of transactions). The cost of an individual transaction is computed based on the zeros and non-zero bytes in its payload. Estimating the L1 Data Fee Scroll has a pre-deployed L1GasPriceOracle at 0x5300000000000000000000000000000000000002, accessible on both Scroll Mainnet and Scroll Sepolia. It provides a getL1Fee method to estimate the L1 data fee for a given transaction’s raw data. function getL1Fee(bytes memory _data) external view override returns (uint256); What happens if gas prices spike on L1? Once the sequencer has processed a transaction, a user’s L1 fee is locked in, and any fluctuations will not affect what a user pays.Because of Scroll’s short block times, any L1 gas changes between a transaction’s submission and its inclusion in a block should be minimal. The sequencer will absorb any changes to L1 gas costs between a transaction’s inclusion in a block and when the sequencer commits the data to L1. Calculating the L1 Data Fee with Gas Oracle As mentioned, the L1GasPriceOracle is used to estimate the L1 gas fee given raw transaction data. This is a push oracle, updated by a relayer run by Scroll. The data fee is based on multiple factors: The bytes which are zeros and nonzeros of an RLP-encoded transaction with Signature l1BaseFee - Current base fee on the L1 overhead - Additional gas overhead of a data commitment transaction scalingFactor - A scaling factor used to account for price spikes PRECISION - A constant used to scale the final fee The following steps are taken to calculate the L1 data fee: Read three fields l1BaseFee, overhead, scalar from the L1GasPriceOracle contract. The slots for these fields in the contract are: FieldSlotl1BaseFee1overhead2scalar3 Count the number of zero bytes and non-zero bytes from the transaction. Calculate the L1 data fee, given TX_DATA_ZERO_GAS = 4 and TX_DATA_NON_ZERO_GAS = 161 and PRECISION = 1e9: l1Gas = zeros * TX_DATA_ZERO_GAS + (nonzeros + 4) * TX_DATA_NON_ZERO_GASl1GasFee = ((l1Gas + overhead) * l1BaseFee * scalar) / PRECISION We reserve an additional 4 bytes in the non-zero bytes to store the number of bytes in the RLP-encoded transaction. L1GasPriceOracle API overhead function overhead() external view returns (uint256); Returns the current L1 fee overhead scalar function scalar() external view returns (uint256); Returns the current l1 fee scalar l1BaseFee function l1BaseFee() external view returns (uint256); Returns the latest known l1 base fee getL1Fee function getL1Fee(bytes memory data) external view returns (uint256); Computes the L1 portion of the fee based on the size of the RLP encoded input transaction, the current L1 base fee, and the various dynamic parameters. Returns: L1 fee that should be paid for the transaction ParameterDescriptiondataSigned fully RLP-encoded transaction to get the L1 fee for. getL1GasUsed function getL1GasUsed(bytes memory data) external view returns (uint256); Computes the amount of L1 gas used for a transaction. Adds the overhead which represents the per-transaction gas overhead of posting the transaction and state roots to L1. Adds 74 bytes of padding to account for the fact that the input does not have a signature. Returns: Amount of L1 gas used to publish the transaction. ParameterDescriptiondataSigned fully RLP-encoded transaction to get the L1 fee for. L1 Originated Transactions When messaging from L1 to L2, the user pays all transaction fees on L1. The user pays L1 gas fees, but because of this, doesn’t need to pay Scroll an L1 Data Fee. They will need to account for L2 Execution Fees in their L1 transaction though, and will need to know how much L2 gas to pay. Contracts on L1 can use an L2 Gas Price Oracle deployed to the L1 to get the gas fee for a given transaction. The oracle also exposes the current l2BaseFee and estimated cross-domain message fee for a given gas limit. On mainnet, the L2GasPriceOracle is deployed at 0x987e300fDfb06093859358522a79098848C33852. On Sepolia, an upgraded L1MessageQueueWithGasPriceOracle should be used, deployed to 0xF0B2293F5D834eAe920c6974D50957A1732de763. If your system supports off-chain mechanisms, you can also call eth_estimateGas and eth_gasPrice on any Scroll RPC node to get an estimate of the gas required for a given transaction. Upgrade Notice After the February 2024 Bridge Upgrade, L2GasPriceOracle will be deprecated in favor of L1MessageQueueWithGasPriceOracle, which will be available at 0x0d7E906BD9cAFa154b048cFa766Cc1E54E39AF9B.The upgrade is expected to be finalized on February 21, 2024, after a two-week timelock. Scroll Sepolia has already undergone this upgrade. Read more here. Future Roadmap Currently, the computation required for proof generation and gas for proof verification on L1 is subsidized by Scroll and various proving partners. It is partially covered by setting a floor to L2 Gas Price. As the prover network becomes decentralized, incentives for proof generation will need to be incorporated into the protocol for the system to be sustainable and scalable. We expect the final gas cost will explicitly include the cost of this proof generation. With further protocol optimization, this cost for the user should be minimal, as each proof covers many transactions. Footnotes See EIP-2028 for more info. ↩ What's Next Contract Deployment Tutorial MoreEdit this pageJoin our community On This PageOverviewL2 FeeCalculating the Execution FeeL1 FeeEstimating the L1 Data FeeCalculating the L1 Data Fee with Gas OracleL1GasPriceOracle APIL1 Originated TransactionsFuture RoadmapFootnotes MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us | Field | Slot | || --- || --- || | l1BaseFee | 1 | | overhead | 2 | | scalar | 3 | | Parameter | Description | || --- || --- || | data | Signed fully RLP-encoded transaction to get the L1 fee for. | | Parameter | Description | || --- || --- || | data | Signed fully RLP-encoded transaction to get the L1 fee for. | ```plaintext l2TransactionExecutionFee = l2TransactionGasUsed * l2TransactionGasPrice ``` ```plaintext function getL1Fee(bytes memory _data) external view override returns (uint256); ``` ```plaintext l1Gas = zeros * TX_DATA_ZERO_GAS + (nonzeros + 4) * TX_DATA_NON_ZERO_GASl1GasFee = ((l1Gas + overhead) * l1BaseFee * scalar) / PRECISION ``` ```plaintext function overhead() external view returns (uint256); ``` ```plaintext function scalar() external view returns (uint256); ``` ```plaintext function l1BaseFee() external view returns (uint256); ``` ```plaintext function getL1Fee(bytes memory data) external view returns (uint256); ``` ```plaintext function getL1GasUsed(bytes memory data) external view returns (uint256); ``` # Tooling deployed on Scroll | Scroll Documentation ## https://docs.scroll.io/en/developers/tooling-deployed-on-scroll Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer Tooling deployed on Scroll On This Page This is a community maintained database of developer tooling that is available on Scroll. We welcome PRs to add or edit information! Products below are not tested by or endorsed by Scroll DefiBridgeDataInfraWalletOnboardingChain AbstractionOracleIdentityDebugAll networks0xDefiEmbed swaps in your on-chain app. ... moreAaveDefi ... moreAcrossBridgeAcross is an interoperability protocol powered by intents, enabling the fastest and lowest-cost way to transfer value without security tradeoffs. ... moreAlchemyDataInfraThe web3 developer platform that provides a suite of tools for building and managing blockchain applications. ... moreArcana NetworkWalletInfraOnboardingChain AbstractionArcana Network Chain Abstraction SDK enables unified balance across chains for app users. ... moreChainlinkOracleBridge ... moreEthereum Attestation ServiceIdentityEthereum Attestation Service (EAS) is an infrastructure public good for making attestations onchain or offchain about anything. ... morePythOraclePyth Network is the largest first-party oracle network, focusing on delivering real-time financial data. ... moreQuickNodeDataInfraA comprehensive platform for web3 data and infrastructure tools, featuring the fastest RPCs, serverless blockchain functions, and robust ETL tools, all supported by enterprise-level reliability and security. ... moreReownOnboardingWalletReown provides developers with tools to enable seamless wallet interactions across their Web3 apps and wallets. ... moreSafeIdentityWallet ... moreSentioDebugData ... moreThe GraphDebugDataGetting historical data on a smart contract can be frustrating when you’re building a dapp. The Graph provides an easy way to query smart contract data through APIs known as subgraphs. ... moreUniswap v3Defi ... moreNo tool selectedTry selecting a tool to learn more about using it on Scroll. 0x is your all-in-one solution for building financial products on crypto rails. We offer a suite of APIs to help developers build faster, get the best prices, and deliver a great user experience. With over 71 million transactions and $148B+ in volume from 8 million+ users, our APIs power top apps like Coinbase Wallet, Robinhood Wallet, Matcha, Metamask, Zerion, and Zapper. 0x provides the Swap API and Gasless API. Get started building with 0x Swap API on Scroll. For Scroll, use 534352 as the chainId. See the 0x cheat sheet for additional information on how to make a 0x API request. You can find contract addresses for Aave on Scroll Mainnet and Scroll Sepolia. Scroll Mainnet contracts: Scroll_SpokePool: 0x3baD7AD0728f9917d1Bf08af5782dCbD516cDd96 MulticallHandler: 0xD2CDF46556543316e7D34e8eDc4624e2bB95e3B6 Scroll Sepolia contracts: Scroll_SpokePool: 0x95B3A7A7344BBd7b1033275CA3Ce89494baA6A40 Get started building on Scroll with Alchemy at docs.alchemy.com/reference/scroll-api-quickstart. Arcana Network offers developers a suite of tools to onboard users from any ecosystem to Scroll, effortlessly. Arcana Chain Abstraction (CA) SDK Arcana CA Wallet (Standalone) The Chain Abstraction feature aggregates user’s USDC, USDT, and ETH balances from other blockchains into a single unified balance that users can spend directly on Scroll without the friction of bridging, allowing for a seamless experience. Arcana Chain Abstraction (CA) SDK: Arcana’s Chain Abstraction SDK can be integrated with any Web3 app on Scroll to enable unified balance for the app users. Unified balance lets users spend their consolidated multi-chain token balance on Scroll. The integration is quick and easy: Minimal changes to the frontend code No smart contract changes required Users can bring existing browser-based third-party wallets No lockups or transfer of assets required Arcana CA Wallet (Standalone): A standalone wallet with built-in chain abstraction. Users can view and spend their unified unified balance across a variety of supported apps. Reach out to us if you’d like your app to be supported by Arcana References Developers: How to integrate a Scroll App with Arcana Chain Abstraction SDK? CA SDK API CA SDK Demo Users: Try the Arcana CA Wallet Arcana Network Website You can find contract addresses for Chainlink for Scroll Mainnet and Scroll Sepolia testnet: LINK Token Contracts for Scroll L2 Sequencer Data Feeds Price Feed Addresses for Scroll Scroll Mainnet contracts: EAS: 0xC47300428b6AD2c7D03BB76D05A176058b47E6B0 SchemaRegistry: 0xD2CDF46556543316e7D34e8eDc4624e2bB95e3B6 EIP712Proxy: 0x77b7DA1c40762Cd8AFfE2069b575328EfD4D9801 Indexer: Not deployed yet Scroll Sepolia contracts: EAS: 0xaEF4103A04090071165F78D45D83A0C0782c2B2a SchemaRegistry: 0x55D26f9ae0203EF95494AE4C170eD35f4Cf77797 EIP712Proxy: 0xB3574f76b1720E61FdA98702c7016674CD6Eaa7b Indexer: 0x7C2cb1eDC328491da52de2a0afc44D3B0Ae7ee17 You can find contract addresses for Pyth for Scroll Mainnet and Scroll Sepolia: How to use Pyth on Scroll Price Feed IDs for Scroll Pyth Scroll Mainnet and Scroll Sepolia contracts QuickNode supercharges your blockchain projects with fast, secure infrastructure built to scale. It supports over 60 chains, low-latency RPC endpoints, flexible serverless functions, and smart ETL tooling that keep your apps running seamlessly. SOC 1 and SOC 2 compliance ensures your data is safe, plus 24/7 global support ready to help when you need it. QuickNode is your go-to partner for unlocking the true potential of Web3 development. Get started for free - Fastest RPC endpoints: docs/scroll Blockchain ETL solution, Streams: docs/streams Blockchain Serverless Functions: docs/functions Resources: Docs Step-by-step guides Video tutorials Reown (prev. known as WalletConnect) provides developers with tools to enable seamless wallet interactions across their Web3 apps and wallets. Reown has two major product offerings, they are, AppKit and WalletKit. AppKit AppKit is a powerful, free, and fully open-source SDK for developers looking to integrate wallet connections and other Web3 functionalities into their apps on any EVM and non-EVM chain. In just a few simple steps, you can provide your users with seamless wallet access, one-click authentication, social logins, and notifications—streamlining their experience while enabling advanced features like on-ramp functionality, in-app token swaps and smart accounts. WalletKit WalletKit is a robust, open-source SDK designed to empower seamless wallet connections and interactions across any blockchain. With WalletKit, you can offer your users a simple and secure way to connect with thousands of apps, enabling features like one-click authentication, secure transaction signing, and streamlined wallet address verification. Its chain-agnostic design ensures effortless multi-chain support, eliminating the need for complex integrations while delivering unmatched connectivity and security. To summarize, AppKit is for Web3 applications and WalletKit is for Web3 wallets. You will be able to use Reown AppKit to power end-to-end wallet interactions on your Web3 app deployed on Scroll. Some links to learn more about Reown: Website Blog Docs Scroll Mainnet is available in the official Safe app, and the transaction service API is at https://safe-transaction-scroll.safe.global. Here are the relevant contracts for Scroll Mainnet: CompatibilityFallbackHandler: 0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4 CreateCall: 0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4 DefaultCallbackHandler: 0x1AC114C2099aFAf5261731655Dc6c306bFcd4Dbd GnosisSafe: 0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552 GnosisSafeL2: 0x3E5c63644E683549055b9Be8653de26E0B4CD36E GnosisSafeProxyFactory: 0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2 MultiSend: 0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761 MultiSendCallOnly: 0x40A2aCCbd92BCA938b02010E17A5b8929b49130D SignMessageLib: 0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2 SimulateTxAccessor: 0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da You can access Safe on Scroll Sepolia here, and the transaction service API is at https://transaction-sepolia.safe.scroll.xyz. Here are the relevant contracts for Scroll Sepolia: CompatibilityFallbackHandler: 0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4 CreateCall: 0x7cbB62EaA69F79e6873cD1ecB2392971036cFAa4 DefaultCallbackHandler: 0x1AC114C2099aFAf5261731655Dc6c306bFcd4Dbd GnosisSafe: 0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552 GnosisSafeL2: 0x3E5c63644E683549055b9Be8653de26E0B4CD36E GnosisSafeProxyFactory: 0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2 MultiSend: 0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761 MultiSendCallOnly: 0x40A2aCCbd92BCA938b02010E17A5b8929b49130D SignMessageLib: 0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2 SimulateTxAccessor: 0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da Sentio provides a powerful, all-in-one solution for transaction debugging, simulation, data indexing, and visualization, offering complete context in a single platform. Sentio Debugger: Best tool to help users understand how a transaction works. It has advanced features such as: Simulation: Run lightning-fast simulations and inspect the simulation result in great detail Dynamic logging: Add custom console.log for transactions to extract specific information, as part of a broader contract override feature Code Insight: Navigate through Solidity code seamlessly, just like in an IDE Sentio Indexer: A state-of-the-art indexing service with integrated user experience: Support both Sentio processor, a blazing fast data indexer powered by the Sentio SDK that streamlines data ingestion, processing, and storage—all with maximum flexibility in TypeScript, as well as hosted subgraph processor Query using SQL/GraphQL and visualize results with an intuitive click-and-drag interface Getting historical data on a smart contract can be frustrating when you’re building a dapp. The Graph provides an easy way to query smart contract data through APIs known as subgraphs. The Graph’s infrastructure relies on a decentralized network of indexers, enabling your dapp to become truly decentralized. The Graph supports both Scroll mainnet & testnet. Quick Start These subgraphs only take a few minutes to set up. To get started, follow these three steps: Initialize your subgraph project Deploy & Publish Query from your dapp Pricing: All developers receive 100K free queries per month on the decentralized network. After these free queries, you only pay based on usage at $4 for every 100K queries. Here’s a step by step walk through that includes screenshots: The Graph Quick Start Scroll Mainnet Main Contracts Core Factory: 0x70C62C8b8e801124A4Aa81ce07b637A3e83cb919 NFT Position Manager: 0xB39002E4033b162fAc607fc3471E205FA2aE5967 Router: 0xfc30937f5cDe93Df8d48aCAF7e6f5D8D8A31F636 Additional Contracts multicall2Address: 0xC1D2e074C38FdD5CA965000668420C80316F0915 proxyAdminAddress: 0x1E6dcAb806A42055098f23E2B3ac72D6E195F967 tickLensAddress: 0x85780e12e90D2a684eB8E7404c985b5B5c8ce7E9 nftDescriptorLibraryAddressV1_3_0: 0xAeE9c206ba89F3DA25EEe4636208519e0B86965B nonfungibleTokenPositionDescriptorAddressV1_3_0: 0xACcf12204b7591B2ECCEFe737440B0f53748B191 descriptorProxyAddress: 0x675DD953225D296A44790dC1390a1E7eF378f464 v3MigratorAddress: 0xF00577B5Dd0DA227298E954Ed11356F264Cf93d4 v3StakerAddress: 0xFdFbE973c9ecB036Ecfb7af697FcACe789D3f928 quoterV2Address: 0x2566e082Cb1656d22BCbe5644F5b997D194b5299 Scroll Sepolia Frontend website: https://uniswap-showcase.sepolia.scroll.xyz/ Main Contracts Core Factory: 0xB856587fe1cbA8600F75F1b1176E44250B11C788 NFT Position Manager: 0xbbAd0e891922A8A4a7e9c39d4cc0559117016fec Router: 0x17AFD0263D6909Ba1F9a8EAC697f76532365Fb95 Additional Contracts multicall2Address: 0x8c181f4B9040F1a2C941EfD3b608712cF86F1957 proxyAdminAddress: 0xD4A9910732b6f301F6F210Ebe7a3dBf16d9E9DD4 tickLensAddress: 0x9804Da978427a49929f2E6Ea32A9594F03f9296e nftDescriptorLibraryAddressV1_3_0: 0x45Bd3B62B7A3aA53371c98049b0f7A9C1A4B5a6c nonfungibleTokenPositionDescriptorAddressV1_3_0: 0x24d4E4a572Dc1e0dbF92a0d7768Ac80df516b2C2 descriptorProxyAddress: 0xa8986417d0EAe50607696b9b0cb7ec5aFBE67765 v3MigratorAddress: 0x38E33D067F03a5cDc02C301b2c306cb0414549Bf v3StakerAddress: 0xe7b82794Cab21e665a3e6f8ea562d392AA6E0619 quoterV2Address: 0xd5dd33650Ef1DC6D23069aEDC8EAE87b0D3619B2 MoreEdit this pageJoin our community On This Page MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us # Contract Deployment Tutorial | Scroll Documentation ## https://docs.scroll.io/en/developers/guides/contract-deployment-tutorial Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer Contract Deployment Tutorial On This PageDeploy contracts with HardhatDeploy contracts with FoundryQuestions and Feedback The Scroll Sepolia Testnet allows anyone to deploy a smart contract on Scroll. In this tutorial, you will learn how to deploy a contract on Scroll Sepolia using common tools for developing on Ethereum. This demo repo illustrates contract deployment with Hardhat and Foundry. Got Testnet ETH? Before you start deploying the contract, you need to request test tokens from a Sepolia faucet and use the bridge to transfer some test ETH from Sepolia to Scroll Sepolia. Alternatively, you could acquire Scroll Sepolia ETH directly. See our Faucet and Bridge guides for help. Deploy contracts with Hardhat If you haven’t already, install nodejs and yarn. Clone the repo and install dependencies: git clone https://github.com/scroll-tech/scroll-guides.gitcd scroll-guides/contract-deploy-demoyarn install Create a .env file following the example .env.example in the root directory. Change PRIVATE_KEY to your own account private key in the .env. Run yarn compile to compile the contract. Run yarn deploy:scrollTestnet to deploy the contract on the Scroll Sepolia Testnet. Run yarn test for hardhat tests. Deploy contracts with Foundry Clone the repo: git clone https://github.com/scroll-tech/scroll-guides.gitcd scroll-guides/contract-deploy-demo Install Foundry: curl -L https://foundry.paradigm.xyz | bashfoundryup Run forge build to build the project. Deploy your contract with Foundry: forge create --rpc-url https://sepolia-rpc.scroll.io/ \ --value \ --constructor-args \ --private-key \ contracts/Lock.sol:Lock is the amount of test ETH to be locked in the contract. Try setting this to some small amount, like 0.0000001ether. is the Unix timestamp after which the funds locked in the contract will become available for withdrawal. Try setting this to some Unix timestamp in the future, like 1696118400 (this Unix timestamp corresponds to October 1, 2023). For example: forge create --rpc-url https://sepolia-rpc.scroll.io/ \ --value 0.00000000002ether \ --constructor-args 1696118400 \ --private-key 0xabc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc1 \ contracts/Lock.sol:Lock Questions and Feedback Thank you for participating in and developing on the Scroll Sepolia Testnet! If you encounter any issues, join our Discord and ask us in the #testnet-devs channel. What's Next Scroll Messenger Cross-chain Interaction MoreEdit this pageJoin our community On This PageDeploy contracts with HardhatDeploy contracts with FoundryQuestions and Feedback MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us ```plaintext git clone https://github.com/scroll-tech/scroll-guides.gitcd scroll-guides/contract-deploy-demoyarn install ``` ```plaintext git clone https://github.com/scroll-tech/scroll-guides.gitcd scroll-guides/contract-deploy-demo ``` ```plaintext curl -L https://foundry.paradigm.xyz | bashfoundryup ``` ```plaintext forge create --rpc-url https://sepolia-rpc.scroll.io/ \ --value \ --constructor-args \ --private-key \ contracts/Lock.sol:Lock ``` ```plaintext forge create --rpc-url https://sepolia-rpc.scroll.io/ \ --value 0.00000000002ether \ --constructor-args 1696118400 \ --private-key 0xabc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc1 \ contracts/Lock.sol:Lock ``` # Scroll Messenger Cross-chain Interaction | Scroll Documentation ## https://docs.scroll.io/en/developers/guides/scroll-messenger-cross-chain-interaction Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer Scroll Messenger Cross-chain Interaction On This PageDeploying the ContractsTarget Smart ContractOperator Smart ContractCalling a Cross-chain FunctionRelay the Message when sending from L2 to L1 In this example, we will launch a dummy smart contract on either Sepolia or Scroll and interact with it from the opposite chain. We will be using the ScrollMessenger that is deployed on both Sepolia and Scroll. Deploying the Contracts Target Smart Contract Let’s start by deploying the target smart contract. We will use the Greeter contract for this example, but you can use any other contract. Deploy it to either Sepolia or Scroll. On Scroll, L1 and L2 use the same API, so it’s up to you. // SPDX-License-Identifier: MITpragma solidity ^0.8.16; // This Greeter contract will be interacted with through the ScrollMessenger across the bridgecontract Greeter { string public greeting = "Hello World!"; // This function will be called by executeFunctionCrosschain on the Operator Smart Contract function setGreeting(string memory greeting_) public { greeting = greeting_; }} We will now execute setGreeting in a cross-chain way. Operator Smart Contract Switch to the other chain and deploy the GreeterOperator. So, if you deployed the Greeter contract on L1, deploy the GreeterOperator on L2 or vice versa. // SPDX-License-Identifier: MITpragma solidity ^0.8.16; // The Scroll Messenger interface is the same on both L1 and L2, it allows sending cross-chain transactions// Let's import it directly from the Scroll Contracts libraryimport "@scroll-tech/contracts@0.1.0/libraries/IScrollMessenger.sol"; // The GreeterOperator is capable of executing the Greeter function through the bridgecontract GreeterOperator { // This function will execute setGreeting on the Greeter contract function executeFunctionCrosschain( address scrollMessengerAddress, address targetAddress, uint256 value, string memory greeting, uint32 gasLimit ) public payable { IScrollMessenger scrollMessenger = IScrollMessenger(scrollMessengerAddress); // sendMessage is able to execute any function by encoding the abi using the encodeWithSignature function scrollMessenger.sendMessage{ value: msg.value }( targetAddress, value, abi.encodeWithSignature("setGreeting(string)", greeting), gasLimit, msg.sender ); }} Calling a Cross-chain Function We pass the message by executing executeFunctionCrosschain and passing the following parameters: scrollMessengerAddress: This will depend on where you deployed the GreeterOperator contract. If you deployed it on Sepolia use 0x50c7d3e7f7c656493D1D76aaa1a836CedfCBB16A. If you deployed on Scroll Sepolia use 0xBa50f5340FB9F3Bd074bD638c9BE13eCB36E603d. targetAddress: The address of the Greeter contract on the opposite chain. value: In this case, it is 0 because the setGreetingis not payable. greeting: This is the parameter that will be sent through the message. Try passing “This message was cross-chain!” gasLimit: If you are sending the message from L1 to L2, around 1000000 gas limit should be more than enough. That said, if you set this too high, and msg.value doesn’t cover gasLimit * baseFee, the transaction will revert. If msg.value is greater than the gas fee, the unused portion will be refunded. If you are sending the message from L2 to L1, pass 0, as the transaction will be completed by executing an additional transaction on L1. Relay the Message when sending from L2 to L1 When a transaction is passed from L2 to L1, an additional “execute withdrawal transaction” must be sent on L1. To do this, you must call relayMessageWithProof on the L1 Scroll Messenger contract from an EOA wallet. You can do this directly on Etherscan Sepolia. To do so, you will need to pass a Merkle inclusion proof for the bridged transaction and other parameters. You’ll query these using the Scroll Bridge API. We’re finalizing the API specifics, but for now, fetch or curl the following endpoint: curl "https://sepolia-api-bridge.scroll.io/api/claimable?page_size=10&page=1&address=GREETER_OPERATOR_ADDRESS_ON_L2" Replace GREETER_OPERATOR_ADDRESS_ON_L2 with your GreeterOperator contract address as launched on L2. Read more about Execute Withdraw transactions in the Scroll Messenger article. Experimental API This API was made for our Bridge UI. It is not yet finalized and may change in the future. We will update this guide when the API is finalized. Anyone can execute your L2 → L1 Message relayMessageWithProof is fully permissionless, so anyone can call it on your behalf if they’re willing to pay the L1 gas fees. This feature allows for additional support infrastructure, including tooling to automate this process for applications and users. After executing and confirming the transaction on both L1 and L2, the new state of greeting on the Greeter contract should be “This message was cross-chain!”. Sending a message from one chain to the other should take around 20 minutes after the transactions are confirmed on the origin chain. Congratulations, you now executed a transaction from one chain to the other using our native bridge! What's Next Bridge ERC20 through the Custom Gateway MoreEdit this pageJoin our community On This PageDeploying the ContractsTarget Smart ContractOperator Smart ContractCalling a Cross-chain FunctionRelay the Message when sending from L2 to L1 MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us ```plaintext // SPDX-License-Identifier: MITpragma solidity ^0.8.16; // This Greeter contract will be interacted with through the ScrollMessenger across the bridgecontract Greeter { string public greeting = "Hello World!"; // This function will be called by executeFunctionCrosschain on the Operator Smart Contract function setGreeting(string memory greeting_) public { greeting = greeting_; }} ``` ```plaintext // SPDX-License-Identifier: MITpragma solidity ^0.8.16; // The Scroll Messenger interface is the same on both L1 and L2, it allows sending cross-chain transactions// Let's import it directly from the Scroll Contracts libraryimport "@scroll-tech/contracts@0.1.0/libraries/IScrollMessenger.sol"; // The GreeterOperator is capable of executing the Greeter function through the bridgecontract GreeterOperator { // This function will execute setGreeting on the Greeter contract function executeFunctionCrosschain( address scrollMessengerAddress, address targetAddress, uint256 value, string memory greeting, uint32 gasLimit ) public payable { IScrollMessenger scrollMessenger = IScrollMessenger(scrollMessengerAddress); // sendMessage is able to execute any function by encoding the abi using the encodeWithSignature function scrollMessenger.sendMessage{ value: msg.value }( targetAddress, value, abi.encodeWithSignature("setGreeting(string)", greeting), gasLimit, msg.sender ); }} ``` ```plaintext curl "https://sepolia-api-bridge.scroll.io/api/claimable?page_size=10&page=1&address=GREETER_OPERATOR_ADDRESS_ON_L2" ``` # Bridge ERC20 through the Custom Gateway | Scroll Documentation ## https://docs.scroll.io/en/developers/guides/bridge-erc20-through-the-custom-gateway Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer Bridge ERC20 through the Custom Gateway On This PageStep 1: Launch a token on SepoliaStep 2: Launch the counterpart token on Scroll Sepolia testnetStep 3: Add the token to the Scroll BridgeStep 4: Deposit tokensStep 5: Withdraw tokensAlternative Approach: Launch and set up a custom L1 gateway contractLaunch an L1 Custom GatewayLaunch an L2 Custom GatewaySetup your Gateway contract on SepoliaSetup your Gateway contract on ScrollBridging tokens This guide will walk through how to use Scroll’s bridge for ERC20s that need custom functionality by using the Custom Gateway. Do you need custom logic? Remember, for many contexts, you won’t need to use the Custom Gateway to add additional features to your token or to its bridging logic. See the article on ETH and ERC20 Token Bridge for more info. Step 1: Launch a token on Sepolia First, we need a token to bridge. There is no need for a particular ERC20 implementation in order for a token to be compatible with L2. If you already have a token, feel free to skip this step. If you want to deploy a new token, use the following contract of a simple ERC20 token that mints 1 million tokens to the deployer when launched. // SPDX-License-Identifier: MITpragma solidity ^0.8.16; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract L1Token is ERC20 { constructor() ERC20("My Token L1", "MTL1") { _mint(msg.sender, 1_000_000 ether); }} Step 2: Launch the counterpart token on Scroll Sepolia testnet Next, you’ll launch a counterpart to this token on Scroll, which will represent the original token on Sepolia. This token can implement custom logic to match that of the L1 token or even add additional features beyond those of the L1 token. For this to work: The token must implement the IScrollStandardERC20 interface in order to be compatible with the bridge. The contract should provide the gateway address and the counterpart token addresses (the L1 token we just launched) under the gateway() and counterpart() functions. It should also allow the L2 gateway to call the token mint() and burn() functions, which are called when a token is deposited and withdrawn. The following is a complete example of a token compatible with the bridge. To the constructor, you will pass the official Scroll Custom Gateway address (0x058dec71E53079F9ED053F3a0bBca877F6f3eAcf) and the address of the token launched on Sepolia. // SPDX-License-Identifier: MITpragma solidity ^0.8.16; import "@openzeppelin/contracts/token/ERC20/ERC20.sol";import "@scroll-tech/contracts@0.1.0/libraries/token/IScrollERC20Extension.sol"; contract L2Token is ERC20, IScrollERC20Extension { // We store the gateway and the L1 token address to provide the gateway() and counterpart() functions which are needed from the Scroll Standard ERC20 interface address _gateway; address _counterpart; // In the constructor we pass as parameter the Custom L2 Gateway and the L1 token address as parameters constructor(address gateway_, address counterpart_) ERC20("My Token L2", "MTL2") { _gateway = gateway_; _counterpart = counterpart_; } function gateway() public view returns (address) { return _gateway; } function counterpart() external view returns (address) { return _counterpart; } // We allow minting only to the Gateway so it can mint new tokens when bridged from L1 function transferAndCall(address receiver, uint256 amount, bytes calldata data) external returns (bool success) { transfer(receiver, amount); data; return true; } // We allow minting only to the Gateway so it can mint new tokens when bridged from L1 function mint(address _to, uint256 _amount) external onlyGateway { _mint(_to, _amount); } // Similarly to minting, the Gateway is able to burn tokens when bridged from L2 to L1 function burn(address _from, uint256 _amount) external onlyGateway { _burn(_from, _amount); } modifier onlyGateway() { require(gateway() == _msgSender(), "Ownable: caller is not the gateway"); _; }} Step 3: Add the token to the Scroll Bridge You need to contact the Scroll team to add the token to L2CustomERC20Gateway contract in Scroll and L1CustomERC20Gateway contract in L1. In addition, follow the instructions on the token lists repository to add your token to the Scroll official bridge frontend. Step 4: Deposit tokens Once your token has been approved by the Scroll team, you should be able to deposit tokens from L1. To do so, you must first approve the L1CustomGateway contract address on Sepolia (0x31C994F2017E71b82fd4D8118F140c81215bbb37). Then, deposit tokens by calling the depositERC20 function from the L1CustomGateway contract. This can be done using our bridge UI, Etherscan Sepolia, or a smart contract. Step 5: Withdraw tokens You will follow similar steps to send tokens back from L2 to L1. First, approve the L2CustomGateway address (0x058dec71E53079F9ED053F3a0bBca877F6f3eAcf) and then withdraw the tokens calling the withdrawERC20 from the L2CustomGateway contract. Alternative Approach: Launch and set up a custom L1 gateway contract Adding your token to the Scroll official bridge (as described above) is the recommended method of bridging tokens to and from Scroll. This approach will make them easier to discover and safer for holders. However, it will require approval from the Scroll team. If you want to launch a custom token without the official approval process, you can launch a custom gateway yourself. To do so, you will need to deploy an L1CustomERC20Gateway contract on L1 and an L2CustomERC20Gateway on L2. Launch an L1 Custom Gateway Let’s start by launching the following contract on Sepolia. // SPDX-License-Identifier: MIT // Although it's possible to use other Solidity versions, we recommend using version 0.8.16 because that's where our contracts were auditedpragma solidity =0.8.16; import "@openzeppelin/contracts/access/Ownable.sol"; import { IL2ERC20Gateway } from "@scroll-tech/contracts@0.1.0/L2/gateways/IL2ERC20Gateway.sol";import { IL1ScrollMessenger } from "@scroll-tech/contracts@0.1.0/L1/IL1ScrollMessenger.sol";import { IL1ERC20Gateway } from "@scroll-tech/contracts@0.1.0/L1/gateways/IL1ERC20Gateway.sol"; import { ScrollGatewayBase } from "@scroll-tech/contracts@0.1.0/libraries/gateway/ScrollGatewayBase.sol";import { L1ERC20Gateway } from "@scroll-tech/contracts@0.1.0/L1/gateways/L1ERC20Gateway.sol"; // This contract will be used to send and receive tokens from L2contract L1CustomERC20Gateway is L1ERC20Gateway, Ownable { // Tokens must be mapped to "bind" them to a token that represents the original token on the original. This event will be emitted when the token mapping for ERC20 token is updated. event UpdateTokenMapping(address indexed l1Token, address indexed oldL2Token, address indexed newL2Token); mapping(address => address) public tokenMapping; constructor() {} // This function must be called once after both the L1 and L2 contract was deployed function initialize(address _counterpart, address _router, address _messenger) external { require(_router != address(0), "zero router address"); ScrollGatewayBase._initialize(_counterpart, _router, _messenger); } /// This function returns the address of the token on L2 function getL2ERC20Address(address _l1Token) public view override returns (address) { return tokenMapping[_l1Token]; } // Updates the token mapping that "binds" a token with another one on the other chain function updateTokenMapping(address _l1Token, address _l2Token) external onlyOwner { require(_l2Token != address(0), "token address cannot be 0"); address _oldL2Token = tokenMapping[_l1Token]; tokenMapping[_l1Token] = _l2Token; emit UpdateTokenMapping(_l1Token, _oldL2Token, _l2Token); } // Callback called before a token is withdrawn on L1 function _beforeFinalizeWithdrawERC20( address _l1Token, address _l2Token, address, address, uint256, bytes calldata ) internal virtual override { require(msg.value == 0, "nonzero msg.value"); require(_l2Token != address(0), "token address cannot be 0"); require(_l2Token == tokenMapping[_l1Token], "l2 token mismatch"); } // Token bridged can be "canceled" or dropped. This callback is called before that happens. function _beforeDropMessage(address, address, uint256) internal virtual override { require(msg.value == 0, "nonzero msg.value"); } // Internal function holding the deposit logic function _deposit( address _token, address _to, uint256 _amount, bytes memory _data, uint256 _gasLimit ) internal virtual override nonReentrant { address _l2Token = tokenMapping[_token]; require(_l2Token != address(0), "no corresponding l2 token"); // 1. Transfer token into this contract. address _from; (_from, _amount, _data) = _transferERC20In(_token, _amount, _data); // 2. Generate message passed to L2CustomERC20Gateway. bytes memory _message = abi.encodeCall( IL2ERC20Gateway.finalizeDepositERC20, (_token, _l2Token, _from, _to, _amount, _data) ); // 3. Send message to L1ScrollMessenger. IL1ScrollMessenger(messenger).sendMessage{ value: msg.value }(counterpart, 0, _message, _gasLimit, _from); emit DepositERC20(_token, _l2Token, _from, _to, _amount, _data); }} Launch an L2 Custom Gateway Now let’s launch the counterpart contract on Scroll. // SPDX-License-Identifier: MIT pragma solidity =0.8.16; import "@openzeppelin/contracts/access/Ownable.sol"; import "@scroll-tech/contracts@0.1.0/L2/gateways/L2ERC20Gateway.sol";import { IL2ScrollMessenger } from "@scroll-tech/contracts@0.1.0/L2/IL2ScrollMessenger.sol";import { IL1ERC20Gateway } from "@scroll-tech/contracts@0.1.0/L1/gateways/IL1ERC20Gateway.sol";import { ScrollGatewayBase } from "@scroll-tech/contracts@0.1.0/libraries/gateway/ScrollGatewayBase.sol";import "@scroll-tech/contracts@0.1.0/libraries/token/IScrollERC20Extension.sol"; import { IL2ERC20Gateway } from "@scroll-tech/contracts@0.1.0/L2/gateways/IL2ERC20Gateway.sol"; // This contract will be used to send and receive tokens from L1contract L2CustomERC20Gateway is L2ERC20Gateway, ScrollGatewayBase, Ownable { event UpdateTokenMapping(address indexed l2Token, address indexed oldL1Token, address indexed newL1Token); // solhint-disable-next-line var-name-mixedcase mapping(address => address) public tokenMapping; constructor() {} // Like with the L1 version of the Gateway, this must be called once after both the L1 and L2 gateways are deployed function initialize(address _counterpart, address _router, address _messenger) external { require(_router != address(0), "zero router address"); ScrollGatewayBase._initialize(_counterpart, _router, _messenger); } /// Returns the address of the token representing the token on L2 function getL1ERC20Address(address _l2Token) external view override returns (address) { return tokenMapping[_l2Token]; } // This returns the L2 token address function getL2ERC20Address(address) public pure override returns (address) { revert("unimplemented"); } // This function finalizes the token deposit on L2 when the deposit was not finalized due to not enough gas sent from L1 function finalizeDepositERC20( address _l1Token, address _l2Token, address _from, address _to, uint256 _amount, bytes calldata _data ) external payable override onlyCallByCounterpart nonReentrant { require(msg.value == 0, "nonzero msg.value"); require(_l1Token != address(0), "token address cannot be 0"); require(_l1Token == tokenMapping[_l2Token], "l1 token mismatch"); IScrollERC20Extension(_l2Token).mint(_to, _amount); _doCallback(_to, _data); emit FinalizeDepositERC20(_l1Token, _l2Token, _from, _to, _amount, _data); } // Same as in the L1 version of this contract, this function "binds" a token with a token on the other chain function updateTokenMapping(address _l2Token, address _l1Token) external onlyOwner { require(_l1Token != address(0), "token address cannot be 0"); address _oldL1Token = tokenMapping[_l2Token]; tokenMapping[_l2Token] = _l1Token; emit UpdateTokenMapping(_l2Token, _oldL1Token, _l1Token); } // Internal function holding the withdraw logic function _withdraw( address _token, address _to, uint256 _amount, bytes memory _data, uint256 _gasLimit ) internal virtual override nonReentrant { address _l1Token = tokenMapping[_token]; require(_l1Token != address(0), "no corresponding l1 token"); require(_amount > 0, "withdraw zero amount"); // 1. Extract real sender if this call is from L2GatewayRouter. address _from = msg.sender; if (router == msg.sender) { (_from, _data) = abi.decode(_data, (address, bytes)); } // 2. Burn token. IScrollERC20Extension(_token).burn(_from, _amount); // 3. Generate message passed to L1StandardERC20Gateway. bytes memory _message = abi.encodeCall( IL1ERC20Gateway.finalizeWithdrawERC20, (_l1Token, _token, _from, _to, _amount, _data) ); // 4. send message to L2ScrollMessenger IL2ScrollMessenger(messenger).sendMessage{ value: msg.value }(counterpart, 0, _message, _gasLimit); emit WithdrawERC20(_l1Token, _token, _from, _to, _amount, _data); }} Setup your Gateway contract on Sepolia Once the contracts are deployed, call the following functions to initialize the contracts and bind them to the corresponding tokens and the gateway on the other side of the bridge. First, call the initialize function on the MyL1Gateway contract with the following parameters: _counterpart: The address of MyL2Gateway we just launched on Scroll. _router: Set it to 0x13FBE0D0e5552b8c9c4AE9e2435F38f37355998a, the L1GatewayRouter contract on Sepolia. _messenger: Set it to 0x50c7d3e7f7c656493D1D76aaa1a836CedfCBB16A, the L1ScrollMessenger contract on Sepolia. A custom gateway can host multiple token bridges. In this case, we will only be allowing bridging between L1Token and L2Token by calling the updateTokenMapping function on the MyL1Gateway contract with the following parameters: _l1Token: The address of the L1Token contract we previously launched on Sepolia. _l2Token: The address of the L2Token contract we previously launched on Scroll. Setup your Gateway contract on Scroll Now let’s switch to the Scroll chain and initialize MyL2Gateway, following similar steps. First, call the initialize function from MyL2Gateway: _counterpart: The address of MyL1Gateway we just launched on Sepolia. _router: Set it to 0x9aD3c5617eCAa556d6E166787A97081907171230, the L2GatewayRouter contract on Scroll. _messenger: Set it 0xBa50f5340FB9F3Bd074bD638c9BE13eCB36E603d, the L2ScrollMessenger contract on Scroll. Next, call updateTokenMapping on the MyL2Gateway contract: _l2Token: The address of the L2Token contract we previously launched on Scroll. _l1Token: The address of the L1Token contract we previously launched on Sepolia. Bridging tokens We can now call depositERC20 from MyL1Gateway and withdrawERC20 from MyL2Gateway just as with the official Scroll bridge. What's Next Running a Scroll Node MoreEdit this pageJoin our community On This PageStep 1: Launch a token on SepoliaStep 2: Launch the counterpart token on Scroll Sepolia testnetStep 3: Add the token to the Scroll BridgeStep 4: Deposit tokensStep 5: Withdraw tokensAlternative Approach: Launch and set up a custom L1 gateway contractLaunch an L1 Custom GatewayLaunch an L2 Custom GatewaySetup your Gateway contract on SepoliaSetup your Gateway contract on ScrollBridging tokens MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us ```plaintext // SPDX-License-Identifier: MITpragma solidity ^0.8.16; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract L1Token is ERC20 { constructor() ERC20("My Token L1", "MTL1") { _mint(msg.sender, 1_000_000 ether); }} ``` ```plaintext // SPDX-License-Identifier: MITpragma solidity ^0.8.16; import "@openzeppelin/contracts/token/ERC20/ERC20.sol";import "@scroll-tech/contracts@0.1.0/libraries/token/IScrollERC20Extension.sol"; contract L2Token is ERC20, IScrollERC20Extension { // We store the gateway and the L1 token address to provide the gateway() and counterpart() functions which are needed from the Scroll Standard ERC20 interface address _gateway; address _counterpart; // In the constructor we pass as parameter the Custom L2 Gateway and the L1 token address as parameters constructor(address gateway_, address counterpart_) ERC20("My Token L2", "MTL2") { _gateway = gateway_; _counterpart = counterpart_; } function gateway() public view returns (address) { return _gateway; } function counterpart() external view returns (address) { return _counterpart; } // We allow minting only to the Gateway so it can mint new tokens when bridged from L1 function transferAndCall(address receiver, uint256 amount, bytes calldata data) external returns (bool success) { transfer(receiver, amount); data; return true; } // We allow minting only to the Gateway so it can mint new tokens when bridged from L1 function mint(address _to, uint256 _amount) external onlyGateway { _mint(_to, _amount); } // Similarly to minting, the Gateway is able to burn tokens when bridged from L2 to L1 function burn(address _from, uint256 _amount) external onlyGateway { _burn(_from, _amount); } modifier onlyGateway() { require(gateway() == _msgSender(), "Ownable: caller is not the gateway"); _; }} ``` ```plaintext // SPDX-License-Identifier: MIT // Although it's possible to use other Solidity versions, we recommend using version 0.8.16 because that's where our contracts were auditedpragma solidity =0.8.16; import "@openzeppelin/contracts/access/Ownable.sol"; import { IL2ERC20Gateway } from "@scroll-tech/contracts@0.1.0/L2/gateways/IL2ERC20Gateway.sol";import { IL1ScrollMessenger } from "@scroll-tech/contracts@0.1.0/L1/IL1ScrollMessenger.sol";import { IL1ERC20Gateway } from "@scroll-tech/contracts@0.1.0/L1/gateways/IL1ERC20Gateway.sol"; import { ScrollGatewayBase } from "@scroll-tech/contracts@0.1.0/libraries/gateway/ScrollGatewayBase.sol";import { L1ERC20Gateway } from "@scroll-tech/contracts@0.1.0/L1/gateways/L1ERC20Gateway.sol"; // This contract will be used to send and receive tokens from L2contract L1CustomERC20Gateway is L1ERC20Gateway, Ownable { // Tokens must be mapped to "bind" them to a token that represents the original token on the original. This event will be emitted when the token mapping for ERC20 token is updated. event UpdateTokenMapping(address indexed l1Token, address indexed oldL2Token, address indexed newL2Token); mapping(address => address) public tokenMapping; constructor() {} // This function must be called once after both the L1 and L2 contract was deployed function initialize(address _counterpart, address _router, address _messenger) external { require(_router != address(0), "zero router address"); ScrollGatewayBase._initialize(_counterpart, _router, _messenger); } /// This function returns the address of the token on L2 function getL2ERC20Address(address _l1Token) public view override returns (address) { return tokenMapping[_l1Token]; } // Updates the token mapping that "binds" a token with another one on the other chain function updateTokenMapping(address _l1Token, address _l2Token) external onlyOwner { require(_l2Token != address(0), "token address cannot be 0"); address _oldL2Token = tokenMapping[_l1Token]; tokenMapping[_l1Token] = _l2Token; emit UpdateTokenMapping(_l1Token, _oldL2Token, _l2Token); } // Callback called before a token is withdrawn on L1 function _beforeFinalizeWithdrawERC20( address _l1Token, address _l2Token, address, address, uint256, bytes calldata ) internal virtual override { require(msg.value == 0, "nonzero msg.value"); require(_l2Token != address(0), "token address cannot be 0"); require(_l2Token == tokenMapping[_l1Token], "l2 token mismatch"); } // Token bridged can be "canceled" or dropped. This callback is called before that happens. function _beforeDropMessage(address, address, uint256) internal virtual override { require(msg.value == 0, "nonzero msg.value"); } // Internal function holding the deposit logic function _deposit( address _token, address _to, uint256 _amount, bytes memory _data, uint256 _gasLimit ) internal virtual override nonReentrant { address _l2Token = tokenMapping[_token]; require(_l2Token != address(0), "no corresponding l2 token"); // 1. Transfer token into this contract. address _from; (_from, _amount, _data) = _transferERC20In(_token, _amount, _data); // 2. Generate message passed to L2CustomERC20Gateway. bytes memory _message = abi.encodeCall( IL2ERC20Gateway.finalizeDepositERC20, (_token, _l2Token, _from, _to, _amount, _data) ); // 3. Send message to L1ScrollMessenger. IL1ScrollMessenger(messenger).sendMessage{ value: msg.value }(counterpart, 0, _message, _gasLimit, _from); emit DepositERC20(_token, _l2Token, _from, _to, _amount, _data); }} ``` ```plaintext // SPDX-License-Identifier: MIT pragma solidity =0.8.16; import "@openzeppelin/contracts/access/Ownable.sol"; import "@scroll-tech/contracts@0.1.0/L2/gateways/L2ERC20Gateway.sol";import { IL2ScrollMessenger } from "@scroll-tech/contracts@0.1.0/L2/IL2ScrollMessenger.sol";import { IL1ERC20Gateway } from "@scroll-tech/contracts@0.1.0/L1/gateways/IL1ERC20Gateway.sol";import { ScrollGatewayBase } from "@scroll-tech/contracts@0.1.0/libraries/gateway/ScrollGatewayBase.sol";import "@scroll-tech/contracts@0.1.0/libraries/token/IScrollERC20Extension.sol"; import { IL2ERC20Gateway } from "@scroll-tech/contracts@0.1.0/L2/gateways/IL2ERC20Gateway.sol"; // This contract will be used to send and receive tokens from L1contract L2CustomERC20Gateway is L2ERC20Gateway, ScrollGatewayBase, Ownable { event UpdateTokenMapping(address indexed l2Token, address indexed oldL1Token, address indexed newL1Token); // solhint-disable-next-line var-name-mixedcase mapping(address => address) public tokenMapping; constructor() {} // Like with the L1 version of the Gateway, this must be called once after both the L1 and L2 gateways are deployed function initialize(address _counterpart, address _router, address _messenger) external { require(_router != address(0), "zero router address"); ScrollGatewayBase._initialize(_counterpart, _router, _messenger); } /// Returns the address of the token representing the token on L2 function getL1ERC20Address(address _l2Token) external view override returns (address) { return tokenMapping[_l2Token]; } // This returns the L2 token address function getL2ERC20Address(address) public pure override returns (address) { revert("unimplemented"); } // This function finalizes the token deposit on L2 when the deposit was not finalized due to not enough gas sent from L1 function finalizeDepositERC20( address _l1Token, address _l2Token, address _from, address _to, uint256 _amount, bytes calldata _data ) external payable override onlyCallByCounterpart nonReentrant { require(msg.value == 0, "nonzero msg.value"); require(_l1Token != address(0), "token address cannot be 0"); require(_l1Token == tokenMapping[_l2Token], "l1 token mismatch"); IScrollERC20Extension(_l2Token).mint(_to, _amount); _doCallback(_to, _data); emit FinalizeDepositERC20(_l1Token, _l2Token, _from, _to, _amount, _data); } // Same as in the L1 version of this contract, this function "binds" a token with a token on the other chain function updateTokenMapping(address _l2Token, address _l1Token) external onlyOwner { require(_l1Token != address(0), "token address cannot be 0"); address _oldL1Token = tokenMapping[_l2Token]; tokenMapping[_l2Token] = _l1Token; emit UpdateTokenMapping(_l2Token, _oldL1Token, _l1Token); } // Internal function holding the withdraw logic function _withdraw( address _token, address _to, uint256 _amount, bytes memory _data, uint256 _gasLimit ) internal virtual override nonReentrant { address _l1Token = tokenMapping[_token]; require(_l1Token != address(0), "no corresponding l1 token"); require(_amount > 0, "withdraw zero amount"); // 1. Extract real sender if this call is from L2GatewayRouter. address _from = msg.sender; if (router == msg.sender) { (_from, _data) = abi.decode(_data, (address, bytes)); } // 2. Burn token. IScrollERC20Extension(_token).burn(_from, _amount); // 3. Generate message passed to L1StandardERC20Gateway. bytes memory _message = abi.encodeCall( IL1ERC20Gateway.finalizeWithdrawERC20, (_l1Token, _token, _from, _to, _amount, _data) ); // 4. send message to L2ScrollMessenger IL2ScrollMessenger(messenger).sendMessage{ value: msg.value }(counterpart, 0, _message, _gasLimit); emit WithdrawERC20(_l1Token, _token, _from, _to, _amount, _data); }} ``` # Running a Scroll L2geth Node | Scroll Documentation ## https://docs.scroll.io/en/developers/guides/running-a-scroll-node Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer Running a Scroll L2geth Node On This PageKey Differences from Scroll SepoliaPrerequisitesStaying Up-to-DateNaming ConventionsHardware RequirementsDockerL1 RPC NodeBuild and Run Using Docker with SnapshotDownload L2gethDownload the SnapshotRun L2gethInteracting with the L2geth ContainerConfiguration ReferenceTroubleshooting This guide will walk you through setting up your own Scroll node using Docker with snapshot. Key Differences from Scroll Sepolia Required binary version: scroll-v5.8.38 or higher. Use --scroll instead of --scroll-sepolia. Mainnet requires a fully synced Ethereum Mainnet RPC for the --l1.endpoint flag. Bootnode setup differs (5 default + 2 optional dedicated ones). Prerequisites Staying Up-to-Date Keep an eye on https://github.com/scroll-tech/go-ethereum/releases for new node releases. Go to https://github.com/scroll-tech/go-ethereum Click on “Watch”, “Custom”, and make sure that “Releases” is selected. We recommend using the latest release at https://github.com/scroll-tech/go-ethereum/releases. Naming Conventions The preferred network name is Scroll or scroll. If you operate a user-facing service (e.g. public RPC endpoint), please consider using this name. Hardware Requirements Recommended Machine comparable to AWS t3.2xlarge instance. 4TB gp3 SSD storage. Currently the archive snapshot contains all the historical data, it’s nearly 2TB, if you download and unarchive it on one disk, you will need at least 4TB disk size. Docker This section assumes you are familiar with Docker and have Docker and Docker Compose installed and running. L1 RPC Node You will need access to a fully-synced Ethereum Mainnet RPC endpoint before running l2geth. You can maintain your own node or choose to use a 3rd-party provider that supports the standard Ethereum RPC APIs. The remainder of the article assumes that you have access to an L1 node’s HTTP RPC API at L2GETH_L1_ENDPOINT. Example: L2GETH_L1_ENDPOINT="https://my-node.com:8545" Build and Run Using Docker with Snapshot Warning Running the node in Docker might have a significant impact on node performance. Download L2geth Download the latest l2geth : docker pull scrolltech/l2geth:latest Download the Snapshot SNAPSHOT_URL = https://scroll-geth-snapshot.s3.us-west-2.amazonaws.com/mpt/latest.tar Create a new folder for the snapshot mkdir l2geth Go to the created folder push l2geth Download the snapshot wget $SNAPSHOT_URL unarchive the snapshot tar -xf latest.tar Run L2geth Go to the snapshot folder cd l2geth In the l2geth folder, setup the docker-compose.yaml config Insert your config in the yaml file and save it, recommended config would be : services: l2geth: image: scrolltech/l2geth:latest container_name: l2geth-docker stop_signal: SIGINT stop_grace_period: 60s restart: "unless-stopped" environment: - CHAIN_ID=534352 - RUST_LOG="info" volumes: - 'l2geth/data:/volume/l2geth/data' ports: - 8545:8545 - 8546:8546 - 6060:6060 - 30303:30303 - 30303:30303/udp command: --scroll --scroll-mpt --snapshot=false --metrics --pprof.addr 0.0.0.0 --pprof.port 6060 --datadir /volume/l2geth/data --gcmode archive --http --http.addr 0.0.0.0 --http.port 8545 --http.api eth,net,web3,debug,scroll --cache.noprefetch --l1.endpoint "${Your l1 endpoint}" --da.blob.blobscan "${Your blobscan endpoint}" --da.blob.beaconnode "${Your beaconnode endpoint}" --cache.snapshot=0 logging: driver: "json-file" options: max-size: "100m" max-file: "1" Run the docker docker compose up -d In a separate shell, you can now attach to l2geth. $ docker exec -it l2geth-docker geth attach "/volume/l2geth-datadir/geth.ipc" > admin.peers.length 5 > eth.blockNumber 10000 Interacting with the L2geth Container Check logs: docker logs --tail 10 -f l2geth-docker Stop the container: docker stop l2geth-docker Fetch metrics: curl http://localhost:6060/debug/metrics/prometheus Configuration Reference SCROLL_L2_CHAIN_ID_L2= 534352 SCROLL_L2_NETWORK_ID_L2= 534352 SCROLL_L2_GENESIS_HASH= 0xbbc05efd412b7cd47a2ed0e5ddfcf87af251e414ea4c801d78b6784513180a80 Bootnode list: enode://c6ac91f43df3d63916ac1ae411cdd5ba249d55d48a7bec7f8cd5bb351a31aba437e5a69e8a1de74d73fdfeba8af1cfe9caf9846ecd3abf60d1ffdf4925b55b23@54.186.123.248:30303enode://fdcc807b5d1353f3a1e98b90208ce6ef1b7d446136e51eaa8ad657b55518a2f8b37655e42375d61622e6ea18f3faf9d070c9bbdf012cf5484bcbad33b7a15fb1@44.227.91.206:30303enode://6beb5a3efbb39be73d17630b6da48e94c0ce7ec665172111463cb470197b20c12faa1fa6f835b81c28571277d1017e65c4e426cc92a46141cf69118ecf28ac03@44.237.194.52:30303enode://7cf893d444eb8e129dca0f6485b3df579911606e7c728be4fa55fcc5f155a37c3ce07d217ccec5447798bde465ac2bdba2cb8763d107e9f3257e787579e9f27e@52.35.203.107:30303enode://c7b2d94e95da343db6e667a01cef90376a592f2d277fbcbf6e9c9186734ed8003d01389571bd10cdbab7a6e5adfa6f0c7b55644d0db24e0b9deb4ec80f842075@54.70.236.187:30303enode://a28f9c5a8f6ed2a5063210a1253ddba9b8b48434e4700198ec8c6e687753dc0eba6dcef4fbbe8a6aeef2079884c2d85ca0bffcab8ec826111576a2cffe7a3266@44.230.97.163:30303enode://0bb5919853cb7cfc302d3c7ac266890cc8f3c67cce7e8fa8d45c3714237fbed395a7b964de131617cf10903774a8ef59d89c002136c2dedad9ef8bd8eddc3ef7@52.36.234.198:30303 Genesis json file can be found here Troubleshooting The node (APIs, geth console, etc) will not be responsive until all the L1 messages have been synced. However, the derivation pipeline is already started during the initial sync phase, which can be seen through L1 sync progress [...] logs. For Scroll Sepolia it might take a little longer (10-20mins) for the first L1 sync progress [...] logs to appear as the L1 blocks are more sparse at the beginning. You should see something like this shortly after starting: INFO [09-18|13:41:34.039] Starting L1 message sync service latestProcessedBlock=20,633,529WARN [09-18|13:41:34.551] Running initial sync of L1 messages before starting l2geth, this might take a while...INFO [09-18|13:41:45.249] Syncing L1 messages processed=20,634,929 confirmed=20,777,179 collected=71 progress(%)=99.315INFO [09-18|13:41:55.300] Syncing L1 messages processed=20,637,029 confirmed=20,777,179 collected=145 progress(%)=99.325INFO [09-18|13:42:05.400] Syncing L1 messages processed=20,638,329 confirmed=20,777,179 collected=220 progress(%)=99.332INFO [09-18|13:42:15.610] Syncing L1 messages processed=20,640,129 confirmed=20,777,179 collected=303 progress(%)=99.340INFO [09-18|13:42:24.324] L1 sync progress "blockchain height"=1000 "block hash"=a28c48..769cee root=174edb..9d9fbdINFO [09-18|13:42:25.555] Syncing L1 messages processed=20,641,529 confirmed=20,777,179 collected=402 progress(%)=99.347 Common Errors Fatal: Failed to create the protocol stack: mkdir /volume/l2geth-datadir: operation not permitted If you run l2geth in Docker and get this error on MacOS, you need to add the /volume directory to the list of shared paths in Docker Desktop settings. Click the Docker icon in the macOS menu bar, then click Settings. Go to the Resources tab. Click on File Sharing. Click the ”+” (plus) button to add a new shared folder. Navigate to the root directory, select the volume folder, and click Open. Click Apply & Restart for the changes to take effect. Fatal: Failed to register the Ethereum service: database contains incompatible genesis (have a4fc62b9b0643e345bdcebe457b3ae898bef59c7203c3db269200055e037afda, new abf4a84278164e166e17c33a64e1e6fea407afd52226b8d1db0878f8774991e8) The genesis config format changed starting at version scroll-v3.1.3. You can fix this error in two ways: Run l2geth with the --scroll flag. This will automatically import the up-to-date genesis config. Alternatively, you can download the latest genesis.json , and run l2geth --datadir "./l2geth-datadir" init "./genesis.json" before running the node. This command will import the updated genesis config. Note: Your local database will remain intact. no matching manifest for linux/arm64/v8 in the manifest list entries If DockerHub does not have the container version required by your system, you can pass --platform linux/amd64 to each Docker command. For better performance, we recommend that you build the image for your platform or Build and Run From Source . Unexpected queue index in SyncService Please make sure your l1 endpoint is working as expected Failed to register the Ethereum service: cannot initialize rollup event sync service: no blob client is configured for rollup verifier Please add working —da.blob.blobscan and —da.blob.beaconnode endpoints Aborting state snapshot generation The issue could be caused by : Disk issue (busy or out of space) Please increase disk space or check why it is busy You recently shut down or restart your machine, mpt-trie has a graceful shutdown issue Add flag --cache.snapshot=0 and --snapshot=false to disable snapshot What's Next Checking Transaction Journey Guide MoreEdit this pageJoin our community On This PageKey Differences from Scroll SepoliaPrerequisitesStaying Up-to-DateNaming ConventionsHardware RequirementsDockerL1 RPC NodeBuild and Run Using Docker with SnapshotDownload L2gethDownload the SnapshotRun L2gethInteracting with the L2geth ContainerConfiguration ReferenceTroubleshooting MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us ```plaintext docker pull scrolltech/l2geth:latest ``` ```plaintext mkdir l2geth ``` ```plaintext push l2geth ``` ```plaintext wget $SNAPSHOT_URL ``` ```plaintext tar -xf latest.tar ``` ```plaintext cd l2geth ``` ```plaintext services: l2geth: image: scrolltech/l2geth:latest container_name: l2geth-docker stop_signal: SIGINT stop_grace_period: 60s restart: "unless-stopped" environment: - CHAIN_ID=534352 - RUST_LOG="info" volumes: - 'l2geth/data:/volume/l2geth/data' ports: - 8545:8545 - 8546:8546 - 6060:6060 - 30303:30303 - 30303:30303/udp command: --scroll --scroll-mpt --snapshot=false --metrics --pprof.addr 0.0.0.0 --pprof.port 6060 --datadir /volume/l2geth/data --gcmode archive --http --http.addr 0.0.0.0 --http.port 8545 --http.api eth,net,web3,debug,scroll --cache.noprefetch --l1.endpoint "${Your l1 endpoint}" --da.blob.blobscan "${Your blobscan endpoint}" --da.blob.beaconnode "${Your beaconnode endpoint}" --cache.snapshot=0 logging: driver: "json-file" options: max-size: "100m" max-file: "1" ``` ```plaintext docker compose up -d ``` ```plaintext $ docker exec -it l2geth-docker geth attach "/volume/l2geth-datadir/geth.ipc" > admin.peers.length 5 > eth.blockNumber 10000 ``` ```plaintext enode://c6ac91f43df3d63916ac1ae411cdd5ba249d55d48a7bec7f8cd5bb351a31aba437e5a69e8a1de74d73fdfeba8af1cfe9caf9846ecd3abf60d1ffdf4925b55b23@54.186.123.248:30303enode://fdcc807b5d1353f3a1e98b90208ce6ef1b7d446136e51eaa8ad657b55518a2f8b37655e42375d61622e6ea18f3faf9d070c9bbdf012cf5484bcbad33b7a15fb1@44.227.91.206:30303enode://6beb5a3efbb39be73d17630b6da48e94c0ce7ec665172111463cb470197b20c12faa1fa6f835b81c28571277d1017e65c4e426cc92a46141cf69118ecf28ac03@44.237.194.52:30303enode://7cf893d444eb8e129dca0f6485b3df579911606e7c728be4fa55fcc5f155a37c3ce07d217ccec5447798bde465ac2bdba2cb8763d107e9f3257e787579e9f27e@52.35.203.107:30303enode://c7b2d94e95da343db6e667a01cef90376a592f2d277fbcbf6e9c9186734ed8003d01389571bd10cdbab7a6e5adfa6f0c7b55644d0db24e0b9deb4ec80f842075@54.70.236.187:30303enode://a28f9c5a8f6ed2a5063210a1253ddba9b8b48434e4700198ec8c6e687753dc0eba6dcef4fbbe8a6aeef2079884c2d85ca0bffcab8ec826111576a2cffe7a3266@44.230.97.163:30303enode://0bb5919853cb7cfc302d3c7ac266890cc8f3c67cce7e8fa8d45c3714237fbed395a7b964de131617cf10903774a8ef59d89c002136c2dedad9ef8bd8eddc3ef7@52.36.234.198:30303 ``` ```plaintext INFO [09-18|13:41:34.039] Starting L1 message sync service latestProcessedBlock=20,633,529WARN [09-18|13:41:34.551] Running initial sync of L1 messages before starting l2geth, this might take a while...INFO [09-18|13:41:45.249] Syncing L1 messages processed=20,634,929 confirmed=20,777,179 collected=71 progress(%)=99.315INFO [09-18|13:41:55.300] Syncing L1 messages processed=20,637,029 confirmed=20,777,179 collected=145 progress(%)=99.325INFO [09-18|13:42:05.400] Syncing L1 messages processed=20,638,329 confirmed=20,777,179 collected=220 progress(%)=99.332INFO [09-18|13:42:15.610] Syncing L1 messages processed=20,640,129 confirmed=20,777,179 collected=303 progress(%)=99.340INFO [09-18|13:42:24.324] L1 sync progress "blockchain height"=1000 "block hash"=a28c48..769cee root=174edb..9d9fbdINFO [09-18|13:42:25.555] Syncing L1 messages processed=20,641,529 confirmed=20,777,179 collected=402 progress(%)=99.347 ``` # Checking Transaction Journey Guide | Scroll Documentation ## https://docs.scroll.io/en/developers/guides/checking-transaction-journey Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer Checking Transaction Journey Guide On This PageArchitecture SummaryEndpointRequest ExampleJourney Field ExampleCommon Failure Reasons This guide will help you to check the journey of a transaction through the Scroll network, how to inspect it via the /tx/journeys API, and interpret its lifecycle. Architecture Summary Diagram of Scroll transaction journey ComponentRoleUser/DAppSubmits transaction.Monitoring ServiceLogs events and tracks transaction progressScroll Internal NodesPeer nodes supporting network operations such as bootnodes or RPC nodes for internal backend services.Scroll SequencerOrders transactions into blocks.HTTP APIExternal access interface for monitoring service Endpoint POST https://venus.scroll.io/v1/tx/journeys Request Example curl -X POST "https://venus.scroll.io/v1/tx/journeys" \ -H "Content-Type: application/json" \ -d '{ "tx_hashes": [ "0xf8a92194cb21dcfaa2efe3230c5d5c80f7e71361575eff33cafa6ebc9eb34357" ] }' | jq Response Fields FieldDescriptiontx_hashTransaction hash queried.current_statusFinal status (queued,pending,executed,dropped,replaced).execution_statusResult of execution (successful,reverted,unknown,error).first_seen_atFirst time the network detected this transaction.last_updated_atMost recent status update time.journeyDetailed list of state transitions for the transaction. Current Status StageDescriptionqueuedTransaction not yet executable, e.g. nonce gap or awaiting prior txs.pendingTransaction valid and executable in mempool.executedTransaction included in a block.droppedTransaction removed due to expiration, mempool overflow, or invalid parameters.replacedTransaction replaced by a new transaction (same sender and nonce, with higher gas price). Execution Status StatusMeaningsuccessfulTransaction executed successfully and included in a block.revertedTransaction failed during execution.unknownTransaction not yet executed.errorUnexpected error in query node. Should rarely occur. Journey Field Example Queued { "event_type": "queued", "timestamp": "2025-06-19T17:22:23.27Z", "node_type": "l2geth-bootnode-0", "event_detail": "Transaction added to future queue (initial processing - will be promoted to pending if nonce is correct and all validity checks pass, or wait for conditions to be met)"} Pending { "event_type": "pending", "timestamp": "2025-06-19T17:22:23.27Z", "node_type": "l2geth-bootnode-0", "event_detail": "Transaction promoted from future queue to pending pool (passed all checks: correct nonce sequence, sufficient balance including L1 data fee, gas limit compliance, and account pending limit)"} Executed { "event_type": "executed", "timestamp": "2025-06-19T17:22:23.299Z", "node_type": "l2geth-mpt-signer-1", "event_detail": "Transaction successfully included in a block and executed on-chain"} Dropped { "event_type": "dropped", "timestamp": "2025-06-19T17:22:23.352Z", "node_type": "l2geth-bootnode-2", "event_detail": "Transaction rejected (validation failed - insufficient funds, unsupported transaction type, or other validation errors)"} Queued: Transaction waiting for execution (e.g., nonce gap) Pending: Transaction ready for inclusion by Scroll Sequencer. Executed: Transaction included in a block. Dropped or Replaced: Transaction invalidated or replaced before execution. Common Failure Reasons Dropped: Timeout: Stuck too long in queue. Mempool full. Invalid parameters (e.g., nonce too low, fee cap below block base fee). Replaced: Same nonce transaction with higher gas price replaces previous. MoreEdit this pageJoin our community On This PageArchitecture SummaryEndpointRequest ExampleJourney Field ExampleCommon Failure Reasons MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us | Component | Role | || --- || --- || | User/DApp | Submits transaction. | | Monitoring Service | Logs events and tracks transaction progress | | Scroll Internal Nodes | Peer nodes supporting network operations such as bootnodes or RPC nodes for internal backend services. | | Scroll Sequencer | Orders transactions into blocks. | | HTTP API | External access interface for monitoring service | | Field | Description | || --- || --- || | tx_hash | Transaction hash queried. | | current_status | Final status (queued,pending,executed,dropped,replaced). | | execution_status | Result of execution (successful,reverted,unknown,error). | | first_seen_at | First time the network detected this transaction. | | last_updated_at | Most recent status update time. | | journey | Detailed list of state transitions for the transaction. | | Stage | Description | || --- || --- || | queued | Transaction not yet executable, e.g. nonce gap or awaiting prior txs. | | pending | Transaction valid and executable in mempool. | | executed | Transaction included in a block. | | dropped | Transaction removed due to expiration, mempool overflow, or invalid parameters. | | replaced | Transaction replaced by a new transaction (same sender and nonce, with higher gas price). | | Status | Meaning | || --- || --- || | successful | Transaction executed successfully and included in a block. | | reverted | Transaction failed during execution. | | unknown | Transaction not yet executed. | | error | Unexpected error in query node. Should rarely occur. | ```plaintext curl -X POST "https://venus.scroll.io/v1/tx/journeys" \ -H "Content-Type: application/json" \ -d '{ "tx_hashes": [ "0xf8a92194cb21dcfaa2efe3230c5d5c80f7e71361575eff33cafa6ebc9eb34357" ] }' | jq ``` ```plaintext { "event_type": "queued", "timestamp": "2025-06-19T17:22:23.27Z", "node_type": "l2geth-bootnode-0", "event_detail": "Transaction added to future queue (initial processing - will be promoted to pending if nonce is correct and all validity checks pass, or wait for conditions to be met)"} ``` ```plaintext { "event_type": "pending", "timestamp": "2025-06-19T17:22:23.27Z", "node_type": "l2geth-bootnode-0", "event_detail": "Transaction promoted from future queue to pending pool (passed all checks: correct nonce sequence, sufficient balance including L1 data fee, gas limit compliance, and account pending limit)"} ``` ```plaintext { "event_type": "executed", "timestamp": "2025-06-19T17:22:23.299Z", "node_type": "l2geth-mpt-signer-1", "event_detail": "Transaction successfully included in a block and executed on-chain"} ``` ```plaintext { "event_type": "dropped", "timestamp": "2025-06-19T17:22:23.352Z", "node_type": "l2geth-bootnode-2", "event_detail": "Transaction rejected (validation failed - insufficient funds, unsupported transaction type, or other validation errors)"} ``` # Stablecoin Payments Tutorial | Scroll Documentation ## https://docs.scroll.io/en/developers/what-to-build/stablecoin-payments-tutorial Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer Stablecoin Payments Tutorial On This PageWhat You’ll BuildInstall and Configure FoundryConfigure Contract DependenciesDeploy the Smart ContractCreate and Configure the React FrontendImplement the Frontend LogicStart the Frontend Welcome to the Scroll Stablecoin Payments Tutorial. This guide walks you through building a dApp that processes USDT payments and mints NFTs as receipts, from smart contract development to frontend integration. Project Source Code You can find the complete working example of this tutorial in our GitHub repository. Feel free to clone it and experiment with the code! What You’ll Build By the time you’re done, you’ll have: Deployed a PaymentProcessor smart contract that handles USDT payments Created a React frontend that enables users to approve and make payments Implemented NFT minting as payment receipts Connected everything to Scroll’s network What you’ll build: a payments app that processes USDT payments and mints NFTs as recipts. Scroll’s infrastructure delivers secure, borderless, fast, and affordable payments, powered by trusted rails like Binance and EtherFi. While Scroll welcomes every dApp, we’re especially committed to solving the unique challenges of emerging markets, explore our work in Africa and Southeast Asia. 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 PaymentProcessor 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# install openzeppelin contractsnpm install @openzeppelin/contracts Configure Contract Dependencies Create a remappings.txt file to help Foundry locate the OpenZeppelin contracts: remappings.txt openzeppelin-contracts/=node_modules/@openzeppelin/contracts/ src/PaymentProcessor.sol // SPDX-License-Identifier: MIT// Compatible with OpenZeppelin Contracts ^5.0.0pragma solidity ^0.8.27; import {ERC721} from "openzeppelin-contracts/token/ERC721/ERC721.sol";import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; // The following contract will process payments from users by debiting the item price and minting an NFT as reciptcontract PaymentProcessor is ERC721 { // Internal NFT counter for ID generation uint _nextTokenId; // NFT metadata url for our demo string _tokenURI = "https://raw.githubusercontent.com/Turupawn/erc721-nft-metadata-demo/refs/heads/main/metadata.json"; // Set to the contract deployer, will receive each USDT payment address public STORE_OWNER; // USDT token address in mainnet address public PAYMENT_TOKEN = 0xf55BEC9cafDbE8730f096Aa55dad6D22d44099Df; // USDT uses 6 decimals, adapt accordingly if you use other token uint8 PAYMENT_TOKEN_DECIMALS = 6; // Item price will cost 0.05 USDT using the formula based on decimal amount uint public ITEM_PRICE = 5 * 10**PAYMENT_TOKEN_DECIMALS / 100; constructor() ERC721("MyToken", "MTK") { STORE_OWNER = msg.sender; } // During puchase, the item price will be debited from the user and transfered to the shop owner function processPurchase() public { uint tokenId = _nextTokenId++; _safeMint(msg.sender, tokenId); IERC20(PAYMENT_TOKEN).transferFrom(msg.sender, address(this), ITEM_PRICE); IERC20(PAYMENT_TOKEN).transfer(STORE_OWNER, ITEM_PRICE); } // Even though in our demo each item has the same metadata we still follow the ERC721 standard function tokenURI(uint tokenId) public view override(ERC721) returns (string memory) { tokenId; return _tokenURI; }} Deploy the Smart Contract warning This tutorial uses Scroll Mainnet with real funds. Please take special care of your wallets: separate developer wallets from your personal or production wallets to avoid any unintended losses. Set your environment variables: .env SCROLL_RPC_URL="https://rpc.scroll.io/"PRIVATE_KEY= Deploy with Foundry: source .envforge create src/PaymentProcessor.sol:PaymentProcessor \ --rpc-url $SCROLL_RPC_URL \ --broadcast \ --private-key $PRIVATE_KEY You should see output confirming the deployer address, contract address, and transaction hash. Save the contract address under Deployed to:, you’ll need it when configuring the frontend. [⠊] Compiling...No files changed, compilation skippedDeployer: 0xbef34f2FCAe62dC3404c3d01AF65a7784c9c4A19Deployed to: 0xf0e9ceCAE516B2F5Ac297B3857453b07455f817FTransaction hash: 0x2bca5934ad82ce26332847fdd6a9241b0da0e38e6928f06499ee58ebc967bbde Create and Configure the React Frontend Scaffold a React app with Vite, wagmi, and Viem: cd ..pnpm create wagmi frontend -t vite-reactcd frontendpnpm install Add your deployed contract address to .env.local: env.local VITE_PAYMENT_PROCESSOR_ADDRESS=0xYourNewContractAddress Implement the Frontend Logic Create a new payments component on src/components/Payments.tsx: src/components/Payments.tsx import { useAccount, useWriteContract } from 'wagmi'import { createPublicClient, http } from 'viem'import { scroll } from 'viem/chains'import { useState, useEffect } from 'react' const PAYMENT_PROCESSOR_ADDRESS = import.meta.env.VITE_PAYMENT_PROCESSOR_ADDRESS as `0x${string}`const USDT_ADDRESS = '0xf55BEC9cafDbE8730f096Aa55dad6D22d44099Df' // Connect your webapp to Scroll Mainnetconst publicClient = createPublicClient({ chain: scroll, transport: http()}) // ABI that allows connecting to our payments smart contractconst paymentProcessorABI = [ { inputs: [], name: "processPurchase", outputs: [], stateMutability: "nonpayable", type: "function", }, { inputs: [], name: "ITEM_PRICE", outputs: [{ type: "uint256" }], stateMutability: "view", type: "function", },] as const // ERC20 ABI to be able to send USDT to our smart contractconst usdtABI = [ { inputs: [ { name: "spender", type: "address" }, { name: "amount", type: "uint256" } ], name: "approve", outputs: [{ type: "bool" }], stateMutability: "nonpayable", type: "function", }, { inputs: [ { name: "owner", type: "address" }, { name: "spender", type: "address" } ], name: "allowance", outputs: [{ type: "uint256" }], stateMutability: "view", type: "function", },] as const // Custom hook for payment functionalityfunction usePayment() { const { address } = useAccount() const [itemPrice, setItemPrice] = useState(null) const [allowance, setAllowance] = useState(null) const { writeContract: writePaymentProcessor, isPending: isProcessing } = useWriteContract() const { writeContract: writeUSDT, isPending: isApproving } = useWriteContract() // Fetches the price of the item set on the smart contract: 0.05 USDT const fetchItemPrice = async () => { try { const result = await publicClient.readContract({ address: PAYMENT_PROCESSOR_ADDRESS, abi: paymentProcessorABI, functionName: 'ITEM_PRICE', }) setItemPrice(result) } catch (error) { console.error('Error reading item price:', error) } } // Check if the user already approved the smart contract const fetchAllowance = async () => { if (!address) return try { const result = await publicClient.readContract({ address: USDT_ADDRESS, abi: usdtABI, functionName: 'allowance', args: [address, PAYMENT_PROCESSOR_ADDRESS], }) setAllowance(result) } catch (error) { console.error('Error reading allowance:', error) } } useEffect(() => { fetchItemPrice() fetchAllowance() }, [address]) // Approves USDT to be sent to our smart contract const approveUSDT = () => { if (!itemPrice) return writeUSDT({ address: USDT_ADDRESS, abi: usdtABI, functionName: 'approve', args: [PAYMENT_PROCESSOR_ADDRESS, itemPrice], }, { onSuccess: (txHash) => { publicClient.waitForTransactionReceipt({ hash: txHash }) .then(() => fetchAllowance()) .catch(console.error) }, }) } // Process the payment const processPurchase = () => { writePaymentProcessor({ address: PAYMENT_PROCESSOR_ADDRESS, abi: paymentProcessorABI, functionName: 'processPurchase', }, { onSuccess: (txHash) => { publicClient.waitForTransactionReceipt({ hash: txHash }) .then(() => fetchAllowance()) .catch(console.error) }, }) } return { itemPrice, allowance, isProcessing, isApproving, approveUSDT, processPurchase, refreshAllowance: fetchAllowance }} // Payment Componentexport function Payments() { const account = useAccount() const { itemPrice, allowance, isProcessing, isApproving, approveUSDT, processPurchase } = usePayment() const needsApproval = allowance !== null && itemPrice !== null && allowance < itemPrice // Displays either the approval or purchase button depending on the state return (

Purchase Item

Price: {itemPrice ? Number(itemPrice) / 1e6 : '...'} USDT

{account.status === 'connected' && ( <> {needsApproval ? ( ) : ( )} )}
)} Add that component to your app: src/App.tsx import { useAccount, useConnect, useDisconnect } from 'wagmi'// Add your new Payments componentimport { Payments } from './components/Payments' function App() { const account = useAccount() const { connectors, connect, error } = useConnect() const { disconnect } = useDisconnect() return (

Wallet

{account.status === 'connected' ? ( <>

Connected: {account.addresses?.[0]}

) : ( <> {connectors.map((connector) => ( ))} {error &&

{error.message}

} )}
// Render your new Payments component
)} export default App Start the Frontend pnpm run dev Open your browser at http://localhost:5173. Connect your wallet, approve USDT, and make a purchase to see your NFT receipt on any marketplace that supports Scroll, such as Element. What's Next Verify Your Smart Contracts MoreEdit this pageJoin our community On This PageWhat You’ll BuildInstall and Configure FoundryConfigure Contract DependenciesDeploy the Smart ContractCreate and Configure the React FrontendImplement the Frontend LogicStart the Frontend MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us ```plaintext # 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# install openzeppelin contractsnpm install @openzeppelin/contracts ``` ```plaintext openzeppelin-contracts/=node_modules/@openzeppelin/contracts/ ``` ```plaintext // SPDX-License-Identifier: MIT// Compatible with OpenZeppelin Contracts ^5.0.0pragma solidity ^0.8.27; import {ERC721} from "openzeppelin-contracts/token/ERC721/ERC721.sol";import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; // The following contract will process payments from users by debiting the item price and minting an NFT as reciptcontract PaymentProcessor is ERC721 { // Internal NFT counter for ID generation uint _nextTokenId; // NFT metadata url for our demo string _tokenURI = "https://raw.githubusercontent.com/Turupawn/erc721-nft-metadata-demo/refs/heads/main/metadata.json"; // Set to the contract deployer, will receive each USDT payment address public STORE_OWNER; // USDT token address in mainnet address public PAYMENT_TOKEN = 0xf55BEC9cafDbE8730f096Aa55dad6D22d44099Df; // USDT uses 6 decimals, adapt accordingly if you use other token uint8 PAYMENT_TOKEN_DECIMALS = 6; // Item price will cost 0.05 USDT using the formula based on decimal amount uint public ITEM_PRICE = 5 * 10**PAYMENT_TOKEN_DECIMALS / 100; constructor() ERC721("MyToken", "MTK") { STORE_OWNER = msg.sender; } // During puchase, the item price will be debited from the user and transfered to the shop owner function processPurchase() public { uint tokenId = _nextTokenId++; _safeMint(msg.sender, tokenId); IERC20(PAYMENT_TOKEN).transferFrom(msg.sender, address(this), ITEM_PRICE); IERC20(PAYMENT_TOKEN).transfer(STORE_OWNER, ITEM_PRICE); } // Even though in our demo each item has the same metadata we still follow the ERC721 standard function tokenURI(uint tokenId) public view override(ERC721) returns (string memory) { tokenId; return _tokenURI; }} ``` ```plaintext SCROLL_RPC_URL="https://rpc.scroll.io/"PRIVATE_KEY= ``` ```plaintext source .envforge create src/PaymentProcessor.sol:PaymentProcessor \ --rpc-url $SCROLL_RPC_URL \ --broadcast \ --private-key $PRIVATE_KEY ``` ```plaintext [⠊] Compiling...No files changed, compilation skippedDeployer: 0xbef34f2FCAe62dC3404c3d01AF65a7784c9c4A19Deployed to: 0xf0e9ceCAE516B2F5Ac297B3857453b07455f817FTransaction hash: 0x2bca5934ad82ce26332847fdd6a9241b0da0e38e6928f06499ee58ebc967bbde ``` ```plaintext cd ..pnpm create wagmi frontend -t vite-reactcd frontendpnpm install ``` ```plaintext VITE_PAYMENT_PROCESSOR_ADDRESS=0xYourNewContractAddress ``` ```plaintext import { useAccount, useWriteContract } from 'wagmi'import { createPublicClient, http } from 'viem'import { scroll } from 'viem/chains'import { useState, useEffect } from 'react' const PAYMENT_PROCESSOR_ADDRESS = import.meta.env.VITE_PAYMENT_PROCESSOR_ADDRESS as `0x${string}`const USDT_ADDRESS = '0xf55BEC9cafDbE8730f096Aa55dad6D22d44099Df' // Connect your webapp to Scroll Mainnetconst publicClient = createPublicClient({ chain: scroll, transport: http()}) // ABI that allows connecting to our payments smart contractconst paymentProcessorABI = [ { inputs: [], name: "processPurchase", outputs: [], stateMutability: "nonpayable", type: "function", }, { inputs: [], name: "ITEM_PRICE", outputs: [{ type: "uint256" }], stateMutability: "view", type: "function", },] as const // ERC20 ABI to be able to send USDT to our smart contractconst usdtABI = [ { inputs: [ { name: "spender", type: "address" }, { name: "amount", type: "uint256" } ], name: "approve", outputs: [{ type: "bool" }], stateMutability: "nonpayable", type: "function", }, { inputs: [ { name: "owner", type: "address" }, { name: "spender", type: "address" } ], name: "allowance", outputs: [{ type: "uint256" }], stateMutability: "view", type: "function", },] as const // Custom hook for payment functionalityfunction usePayment() { const { address } = useAccount() const [itemPrice, setItemPrice] = useState(null) const [allowance, setAllowance] = useState(null) const { writeContract: writePaymentProcessor, isPending: isProcessing } = useWriteContract() const { writeContract: writeUSDT, isPending: isApproving } = useWriteContract() // Fetches the price of the item set on the smart contract: 0.05 USDT const fetchItemPrice = async () => { try { const result = await publicClient.readContract({ address: PAYMENT_PROCESSOR_ADDRESS, abi: paymentProcessorABI, functionName: 'ITEM_PRICE', }) setItemPrice(result) } catch (error) { console.error('Error reading item price:', error) } } // Check if the user already approved the smart contract const fetchAllowance = async () => { if (!address) return try { const result = await publicClient.readContract({ address: USDT_ADDRESS, abi: usdtABI, functionName: 'allowance', args: [address, PAYMENT_PROCESSOR_ADDRESS], }) setAllowance(result) } catch (error) { console.error('Error reading allowance:', error) } } useEffect(() => { fetchItemPrice() fetchAllowance() }, [address]) // Approves USDT to be sent to our smart contract const approveUSDT = () => { if (!itemPrice) return writeUSDT({ address: USDT_ADDRESS, abi: usdtABI, functionName: 'approve', args: [PAYMENT_PROCESSOR_ADDRESS, itemPrice], }, { onSuccess: (txHash) => { publicClient.waitForTransactionReceipt({ hash: txHash }) .then(() => fetchAllowance()) .catch(console.error) }, }) } // Process the payment const processPurchase = () => { writePaymentProcessor({ address: PAYMENT_PROCESSOR_ADDRESS, abi: paymentProcessorABI, functionName: 'processPurchase', }, { onSuccess: (txHash) => { publicClient.waitForTransactionReceipt({ hash: txHash }) .then(() => fetchAllowance()) .catch(console.error) }, }) } return { itemPrice, allowance, isProcessing, isApproving, approveUSDT, processPurchase, refreshAllowance: fetchAllowance }} // Payment Componentexport function Payments() { const account = useAccount() const { itemPrice, allowance, isProcessing, isApproving, approveUSDT, processPurchase } = usePayment() const needsApproval = allowance !== null && itemPrice !== null && allowance < itemPrice // Displays either the approval or purchase button depending on the state return (

Purchase Item

Price: {itemPrice ? Number(itemPrice) / 1e6 : '...'} USDT

{account.status === 'connected' && ( <> {needsApproval ? ( ) : ( )} )}
)} ``` ```plaintext import { useAccount, useConnect, useDisconnect } from 'wagmi'// Add your new Payments componentimport { Payments } from './components/Payments' function App() { const account = useAccount() const { connectors, connect, error } = useConnect() const { disconnect } = useDisconnect() return (

Wallet

{account.status === 'connected' ? ( <>

Connected: {account.addresses?.[0]}

) : ( <> {connectors.map((connector) => ( ))} {error &&

{error.message}

} )}
// Render your new Payments component
)} export default App ``` ```plaintext pnpm run dev ``` # Solidity Cookbook | Scroll Documentation ## https://docs.scroll.io/en/developers/what-to-build/solidity-cookbook Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer Solidity Cookbook On This PageLend on AaveQuery Chainlink Price FeedsAttest to Anything on Ethereum Attestation ServiceSwap Tokens on Uniswap V3 Smart contracts are open and interoperable by default. Scroll’s ecosystem includes both established blue-chip protocols and native projects. Below is a collection of Solidity code snippets you can use to easily integrate DeFi and more into your contracts. Did we miss an important protocol, or do you want to add your own? Send us a PR here. Lend on Aave Supplying assets to Aave serves two functions: earning yield and providing collateral for loans. By depositing any idle tokens from your protocol into Aave, you create a sustainable revenue stream either for your platform or your users. Additionally, you can integrate Aave’s lending features directly into your app or abstract the borrowing experience behind your own interface. In the following example we will supply USDC to Aave to generate yield. You will be able to see your supplied assets and also other Markets available at app.aave.com. Currently ETH, USDC, weETH, wstETH and SCR are supported. Learn more at Aave’s official docs. // SPDX-License-Identifier: MITpragma solidity ^0.8.13; // Import Openzeppelin's IERC20 to interact with XXX and Aave's IPool to interact with the supply APIimport {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";import {IPool} from "https://github.com/aave/aave-v3-core/blob/master/contracts/interfaces/IPool.sol"; // Demonstrates how to supply tokens into the Scroll Aave poolscontract AaveScrollDemo { address public immutable AAVE_POOL_ADDRESS = 0x11fCfe756c05AD438e312a7fd934381537D3cFfe; // Aave pool on Scroll Mainnet address public immutable USDC_ADDRESS = 0x06eFdBFf2a14a7c8E15944D1F4A48F9F95F663A4; // USDC token on Scroll Mainnet // Supply tokens to the Aave pool on behalf of the sender // Important: You need to approve this contract before calling this, also remember USDC uses 6 decimals. Approve here: https://scrollscan.com/token/0x06efdbff2a14a7c8e15944d1f4a48f9f95f663a4#writeProxyContract#F1 function stake(uint amount) public { // First we transfer the USDC from the sender into this contract and approve the Aave Pool IERC20(USDC_ADDRESS).transferFrom(msg.sender, address(this), amount); IERC20(USDC_ADDRESS).approve(AAVE_POOL_ADDRESS, amount); // Next we call the supply function IPool(AAVE_POOL_ADDRESS).supply( USDC_ADDRESS, amount, msg.sender, 0); // After the transaction succeeds you should be able to see your supply at app.aave.com }} Query Chainlink Price Feeds Chainlink has been an historical backbone in DeFi. In this demo we query the bitcoin price on Scroll Mainnet. See the complete list of price feeds in the official documentation. // SPDX-License-Identifier: MITpragma solidity ^0.8.7; // Import Chainlink's aggregator interface that exposes many data feedsimport {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; // Demonstrates how to query Bitcoin price in Scroll Mainnetcontract ChainlinkScrollDemo { // Connect your contract with the Bitcoin price provider AggregatorV3Interface internal dataFeed = AggregatorV3Interface( 0xCaca6BFdeDA537236Ee406437D2F8a400026C589 ); // View function that returns the current Bitcoin price function getChainlinkDataFeedLatestAnswer() public view returns (int) { // prettier-ignore ( /* uint80 roundID */, int answer, /*uint startedAt*/, /*uint timeStamp*/, /*uint80 answeredInRound*/ ) = dataFeed.latestRoundData(); return answer; }} Attest to Anything on Ethereum Attestation Service Define custom schemas and issue attestations for anything, from digital identities to DeFi events, by using the EAS. You can interact with EAS directly on Scroll’s EAScan or integrate it into your own smart contracts. Next, we’ll demonstrate how to issue a “friendship attestation” from a smart contract, certifying that the controller of a given address is your friend. For full details on EAS and its APIs, refer to the official documentation. // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import { IEAS, AttestationRequest, AttestationRequestData, RevocationRequest, RevocationRequestData } from "@ethereum-attestation-service/eas-contracts/contracts/IEAS.sol";import { NO_EXPIRATION_TIME, EMPTY_UID } from "@ethereum-attestation-service/eas-contracts/contracts/Common.sol"; contract EASScrollDemo{ address easAddress = 0xaEF4103A04090071165F78D45D83A0C0782c2B2a; bytes32 schema = 0x27d06e3659317e9a4f8154d1e849eb53d43d91fb4f219884d1684f86d797804a; // check at https://scroll-sepolia.easscan.org/schema/view/0x27d06e3659317e9a4f8154d1e849eb53d43d91fb4f219884d1684f86d797804a function sendIsFriend(address to, bool isFriend) public returns(bytes32) { return IEAS(easAddress).attest( AttestationRequest({ schema: schema, data: AttestationRequestData({ recipient: to, expirationTime: NO_EXPIRATION_TIME, revocable: false, refUID: EMPTY_UID, data: abi.encode(isFriend), value: 0 // No value/ETH }) }) ); }} Swap Tokens on Uniswap V3 Uniswap V3 is a leading DEX protocol that enables token swaps and concentrated liquidity. You can integrate Uniswap V3 swaps directly into your contracts using the ISwapRouter interface. Below is an example of how to swap USDC for WETH on Scroll Mainnet. Important note: keep in mind that currently, in Scroll, most liquidity is onther DEXes other than Uniswap V3. Learn more at Uniswap V3 Core Docs. // SPDX-License-Identifier: MITpragma solidity ^0.8.13; // Import the OpenZeppelin's IERC20 so we can interacts with ERC20 tokensimport {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; // Uniswap v3 Router (Rollups version) deployed on Scrollinterface ISwapRouter { struct ExactInputSingleParams { address tokenIn; address tokenOut; uint24 fee; address recipient; uint256 amountIn; uint256 amountOutMinimum; uint160 sqrtPriceLimitX96; } function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut);} // Demonstrates how to make a swap from within a smart contract on uniswap v3 on Scroll Mainnetcontract UniV3ScrollDemo { ISwapRouter public immutable swapRouter = ISwapRouter(0xfc30937f5cDe93Df8d48aCAF7e6f5D8D8A31F636); address public constant USDC = 0x06eFdBFf2a14a7c8E15944D1F4A48F9F95F663A4; // USDC on Scroll Mainnet address public constant WETH9 = 0x5300000000000000000000000000000000000004; // WETH on Scroll Mainnet uint24 public constant poolFee = 500; // 0.05% // Swaps a specified amount of USDC for WETH function swapUSDCForWETH(uint256 amountIn) external returns (uint256 amountOut) { // Transfer DAI from sender to this contract and approve the Uniswap Router IERC20(USDC).transferFrom(msg.sender, address(this), amountIn); IERC20(USDC).approve(address(swapRouter), amountIn); // Set up the swap parameters ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ tokenIn: USDC, tokenOut: WETH9, fee: poolFee, recipient: msg.sender, amountIn: amountIn, amountOutMinimum: 0, // WARNING: Set to 0 for simplicity; consider slippage in production sqrtPriceLimitX96: 0 }); // Execute the swap amountOut = swapRouter.exactInputSingle(params); }} What's Next Verify Your Smart Contracts MoreEdit this pageJoin our community On This PageLend on AaveQuery Chainlink Price FeedsAttest to Anything on Ethereum Attestation ServiceSwap Tokens on Uniswap V3 MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us ```plaintext // SPDX-License-Identifier: MITpragma solidity ^0.8.13; // Import Openzeppelin's IERC20 to interact with XXX and Aave's IPool to interact with the supply APIimport {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";import {IPool} from "https://github.com/aave/aave-v3-core/blob/master/contracts/interfaces/IPool.sol"; // Demonstrates how to supply tokens into the Scroll Aave poolscontract AaveScrollDemo { address public immutable AAVE_POOL_ADDRESS = 0x11fCfe756c05AD438e312a7fd934381537D3cFfe; // Aave pool on Scroll Mainnet address public immutable USDC_ADDRESS = 0x06eFdBFf2a14a7c8E15944D1F4A48F9F95F663A4; // USDC token on Scroll Mainnet // Supply tokens to the Aave pool on behalf of the sender // Important: You need to approve this contract before calling this, also remember USDC uses 6 decimals. Approve here: https://scrollscan.com/token/0x06efdbff2a14a7c8e15944d1f4a48f9f95f663a4#writeProxyContract#F1 function stake(uint amount) public { // First we transfer the USDC from the sender into this contract and approve the Aave Pool IERC20(USDC_ADDRESS).transferFrom(msg.sender, address(this), amount); IERC20(USDC_ADDRESS).approve(AAVE_POOL_ADDRESS, amount); // Next we call the supply function IPool(AAVE_POOL_ADDRESS).supply( USDC_ADDRESS, amount, msg.sender, 0); // After the transaction succeeds you should be able to see your supply at app.aave.com }} ``` ```plaintext // SPDX-License-Identifier: MITpragma solidity ^0.8.7; // Import Chainlink's aggregator interface that exposes many data feedsimport {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; // Demonstrates how to query Bitcoin price in Scroll Mainnetcontract ChainlinkScrollDemo { // Connect your contract with the Bitcoin price provider AggregatorV3Interface internal dataFeed = AggregatorV3Interface( 0xCaca6BFdeDA537236Ee406437D2F8a400026C589 ); // View function that returns the current Bitcoin price function getChainlinkDataFeedLatestAnswer() public view returns (int) { // prettier-ignore ( /* uint80 roundID */, int answer, /*uint startedAt*/, /*uint timeStamp*/, /*uint80 answeredInRound*/ ) = dataFeed.latestRoundData(); return answer; }} ``` ```plaintext // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import { IEAS, AttestationRequest, AttestationRequestData, RevocationRequest, RevocationRequestData } from "@ethereum-attestation-service/eas-contracts/contracts/IEAS.sol";import { NO_EXPIRATION_TIME, EMPTY_UID } from "@ethereum-attestation-service/eas-contracts/contracts/Common.sol"; contract EASScrollDemo{ address easAddress = 0xaEF4103A04090071165F78D45D83A0C0782c2B2a; bytes32 schema = 0x27d06e3659317e9a4f8154d1e849eb53d43d91fb4f219884d1684f86d797804a; // check at https://scroll-sepolia.easscan.org/schema/view/0x27d06e3659317e9a4f8154d1e849eb53d43d91fb4f219884d1684f86d797804a function sendIsFriend(address to, bool isFriend) public returns(bytes32) { return IEAS(easAddress).attest( AttestationRequest({ schema: schema, data: AttestationRequestData({ recipient: to, expirationTime: NO_EXPIRATION_TIME, revocable: false, refUID: EMPTY_UID, data: abi.encode(isFriend), value: 0 // No value/ETH }) }) ); }} ``` ```plaintext // SPDX-License-Identifier: MITpragma solidity ^0.8.13; // Import the OpenZeppelin's IERC20 so we can interacts with ERC20 tokensimport {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; // Uniswap v3 Router (Rollups version) deployed on Scrollinterface ISwapRouter { struct ExactInputSingleParams { address tokenIn; address tokenOut; uint24 fee; address recipient; uint256 amountIn; uint256 amountOutMinimum; uint160 sqrtPriceLimitX96; } function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut);} // Demonstrates how to make a swap from within a smart contract on uniswap v3 on Scroll Mainnetcontract UniV3ScrollDemo { ISwapRouter public immutable swapRouter = ISwapRouter(0xfc30937f5cDe93Df8d48aCAF7e6f5D8D8A31F636); address public constant USDC = 0x06eFdBFf2a14a7c8E15944D1F4A48F9F95F663A4; // USDC on Scroll Mainnet address public constant WETH9 = 0x5300000000000000000000000000000000000004; // WETH on Scroll Mainnet uint24 public constant poolFee = 500; // 0.05% // Swaps a specified amount of USDC for WETH function swapUSDCForWETH(uint256 amountIn) external returns (uint256 amountOut) { // Transfer DAI from sender to this contract and approve the Uniswap Router IERC20(USDC).transferFrom(msg.sender, address(this), amountIn); IERC20(USDC).approve(address(swapRouter), amountIn); // Set up the swap parameters ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ tokenIn: USDC, tokenOut: WETH9, fee: poolFee, recipient: msg.sender, amountIn: amountIn, amountOutMinimum: 0, // WARNING: Set to 0 for simplicity; consider slippage in production sqrtPriceLimitX96: 0 }); // Execute the swap amountOut = swapRouter.exactInputSingle(params); }} ``` # Privacy dApps with ZK | Scroll Documentation ## https://docs.scroll.io/en/developers/what-to-build/privacy-dapps-with-zk Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer Privacy dApps with ZK On This PageDependencies1. Create a circuit2. Deploy the contracts3. Build a frontend Scroll has been a pioneer in zk dapp support from the start, enabling tools like Circom, Noir, and ZoKrates. Today, we’re building a privacy-focused app that leverages Scroll’s low fees, fast blocks, and zk-native guarantees to unlock new use cases. Users need privacy in finance, identity, and social interactions but web3 is public by design. The solution is browser-based proving: generating zk proofs locally, before any data touches the internet. This keeps user data secure and private by default. In order to keep the paramaters private, they should never get out of the browser Let’s get to know, with a practical and simple example, how to create interfaces that make use of zk-wasm, the technology that makes this possible. Dependencies For this example, we will use Circom. If you don’t have it installed, you can do so with the following commands. curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | shgit clone https://github.com/iden3/circom.gitcd circomcargo build --releasecargo install --path circomnpm install -g snarkjs 1. Create a circuit We’ll create a very simple example: generating a computation proof for a multiplication a*b=c while keeping a and b private. This will make a solid starting point before building real use cases. Circom allows us to create circuits that generate execution proofs while obfuscating the parameters. Start by creating the following circuit: myCircuit.circom pragma circom 2.0.0; template Multiplier() { signal input a; signal input b; signal output c; c <== a*b; } component main = Multiplier(); Now compile it and generate the artifacts that we will use later. circom myCircuit.circom --r1cs --wasm --symsnarkjs powersoftau new bn128 12 pot12_0000.ptau -vsnarkjs powersoftau contribute pot12_0000.ptau pot12_0001.ptau --name="First contribution" -vsnarkjs powersoftau prepare phase2 pot12_0001.ptau pot12_final.ptau -vsnarkjs groth16 setup myCircuit.r1cs pot12_final.ptau myCircuit_0000.zkeysnarkjs zkey contribute myCircuit_0000.zkey myCircuit_0001.zkey --name="1st Contributor Name" -vsnarkjs zkey export verificationkey myCircuit_0001.zkey verification_key.json 2. Deploy the contracts The following command will generate a verifier contract in the verifier.sol file. Deploy it on Scroll testnet by using the framework of your choice (we recommend either Remix for a borwser experience or Foundry as detailed in our getting started guide). This contract contains the verifyProof() function, which takes a computation proof made with our circuit as a parameter and returns true if the proof is correct. snarkjs zkey export solidityverifier myCircuit_0001.zkey verifier.sol Now deploy the following custom logic contract, passing the address of the verifier contract we deployed earlier as a constructor parameter. In this contract, you can add any desired logic in Solidity, such as vote counting in a voting system or the reception or sending of ERC20 tokens in an anonymous DeFi system. In this example, we will only store the result of the multiplication we did in our circuit. // SPDX-License-Identifier: MIT pragma solidity >=0.7.0 <0.9.0; // Interface that allows verifing proofs by sending them as a call to the verifier contract we just deployedinterface ICircomVerifier { function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[1] calldata _pubSignals) external view returns (bool);} // Contract that demonstrates the typical structure of a ZK verifer with custom logic in Soliditycontract CircomCustomLogic { ICircomVerifier circomVerifier; uint public publicInput; // Recieves a valid circom verifer contract address that was autogenerated by snarkjs constructor(address circomVeriferAddress) { circomVerifier = ICircomVerifier(circomVeriferAddress); } // Typical proof verifying function, execute custom logic if the proof passed as parameter is correct function sendProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[1] calldata _pubSignals) public { // ZK verification circomVerifier.verifyProof(_pA, _pB, _pC, _pubSignals); // Your custom logic, in this case just storing the multiplication result publicInput = _pubSignals[0]; }} 3. Build a frontend Let’s start by creating a new wagmi frontend in react with the snarkjs dependency that will help us building ZK proofs. cd ..pnpm create wagmi zk-tutorial -t vite-reactcd frontendpnpm install snarkjs Now create this file structure where you populate the zk_artifacts with the build result in your circuit drectory and add the typescript files as detailed below: zk-tutorial/├── public/│ └── zk_artifacts/│ ├── myCircuit.wasm│ ├── myCircuit_final.zkey│ └── verification_key.json├── src/│ ├── components/│ │ └── ZKForm.tsx│ └── App.tsx├── index.html├── tsconfig.json├── env.local└── package.json Import the ZK Artifacts Put the myCircuit.wasm, myCircuit_final.zkey and verification_key.json generated previously in the circuit directory into a new folder called public/zk_artifacts/. Notice we changed the myCircuit_0001.zkey name for myCircuit_final.zkey Add the address of the CircomCustomLogic you just deployed to the environment files. zk-tutorial/env.local VITE_COUNTER_CONTRACT_ADDRESS=0xYourNewContractAddress Now implement the ZK and web3 logic in a new component. zk-tutorial/src/components/ZKForm.tsx src/components/ZKForm.tsximport { useAccount, useWriteContract } from 'wagmi'import { createPublicClient, http } from 'viem'import { scrollSepolia } from 'viem/chains'import { useState, useEffect } from 'react'import { groth16 } from "snarkjs"; // Custom logic contract we just deployedconst CONTRACT_ADDRESS = import.meta.env.VITE_CIRCOM_CUSTOM_LOGIC_CONTRACT_ADDRESS as `0x${string}` // In this tutorial we'll use Scroll Sepolia, but this code can also be used on Scroll Mainnet or any EVM chain that supports all precompiles needed for ZK proofconst publicClient = createPublicClient({ chain: scrollSepolia, transport: http()}) // Custom logic contract ABI to be able to send proofs on-chainconst verifierAbi = [ { "inputs": [ { "internalType": "uint256[2]", "name": "_pA", "type": "uint256[2]" }, { "internalType": "uint256[2][2]", "name": "_pB", "type": "uint256[2][2]" }, { "internalType": "uint256[2]", "name": "_pC", "type": "uint256[2]" }, { "internalType": "uint256[1]", "name": "_pubSignals", "type": "uint256[1]" } ], "name": "sendProof", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "publicInput", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }] as const // Function responsible to generate private computation on the browser and then sending the computation proof on-chainfunction useZKForm() { const [a, setA] = useState("") const [b, setB] = useState("") const [message, setMessage] = useState("Connect your wallet") const [publicInput, setPublicInput] = useState(null) const { writeContract, isPending: isSendingProof } = useWriteContract() // Reads the contract state, in this case the multiplication result that was previously submmited const fetchPublicInput = async () => { try { const result = await publicClient.readContract({ address: CONTRACT_ADDRESS, abi: verifierAbi, functionName: "publicInput", }) setPublicInput(Number(result)) } catch (err) { console.error("Error fetching public input:", err) } } // Generates the ZK proof based on private parameters, and then sends the proofs to the smart contract on Scroll const sendProof = async () => { setMessage("Generating proof...") // Let's start by generating a proof by passing private inputs and the ZK artifacts generated by the circom compiler // Notice the ZK artifacts should be shared publicly in your website const { proof, publicSignals } = await groth16.fullProve( { a: Number(a), b: Number(b) }, "./zk_artifacts/myCircuit.wasm", "./zk_artifacts/myCircuit_final.zkey" ) // Now let's verify the proof locally, this is optional setMessage("Verifying off‑chain...") const vkey = await fetch("/verification_key.json").then((r) => r.json() ) const valid = await groth16.verify(vkey, publicSignals, proof) if (!valid) { setMessage("Proof verification failed") return } // We now translate the proof in a format compatible with what our smart contract expects const pA = proof.pi_a.slice(0, 2) // Take first two elements const pB = proof.pi_b.slice(0, 2).map((row: string[]) => row.slice(0, 2)) const pC = proof.pi_c.slice(0, 2) // Once the proof is ready we send it on-chain setMessage("Proof generated please confirm transaction.") writeContract({ address: CONTRACT_ADDRESS, abi: verifierAbi, functionName: "sendProof", args: [pA, pB, pC, publicSignals], }, { onSuccess: (txHash) => { setMessage("Executing...") publicClient.waitForTransactionReceipt({ hash: txHash }) .then(() => { setMessage("Success!") fetchPublicInput() }) .catch((err) => { console.error("ERROR! Transaction reverted:", err) setMessage("Transaction failed") }) }, onError: (err) => { console.error("ERROR! Transaction reverted:", err) setMessage("Transaction failed") } }) } return { a, setA, b, setB, message, publicInput, isSendingProof, sendProof, fetchPublicInput }} // React UI that handles contract reads, and private inputs for the ZK proof generationexport default function ZKForm() { const account = useAccount() const { a, setA, b, setB, message, publicInput, isSendingProof, sendProof, fetchPublicInput } = useZKForm() useEffect(() => { if (account.status === 'connected') { fetchPublicInput() } }, [account.status, fetchPublicInput]) return (

ZK Multiplication

{message}

setA(e.target.value)} /> setB(e.target.value)} /> {account.status === 'connected' && ( )} {publicInput !== null && (

Last stored result: {publicInput}

)}
)} Add the new component to your app. zk-tutorial/src/App.tsx import { useAccount, useConnect, useDisconnect } from 'wagmi'// Import your ZKFormimport ZKForm from "./components/ZKForm"; function App() { const account = useAccount() const { connectors, connect, status, error } = useConnect() const { disconnect } = useDisconnect() return ( <>

Account

status: {account.status}
addresses: {JSON.stringify(account.addresses)}
chainId: {account.chainId}
{account.status === 'connected' && ( )}

Connect

{connectors.map((connector) => ( ))}
{status}
{error?.message}

ZK Tutorial

)} export default App Finally, start the frontend. pnpm run dev Once everything is ready this is how your app should look like Now that you know the core parts of a zkDapp you can start experimenting with real use cases. ZK has demonstrated to serve DeFi, DiD, gamming and more. We’re excited to see what you will build on Scroll! What's Next Verify Your Smart Contracts MoreEdit this pageJoin our community On This PageDependencies1. Create a circuit2. Deploy the contracts3. Build a frontend MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us ```plaintext curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | shgit clone https://github.com/iden3/circom.gitcd circomcargo build --releasecargo install --path circomnpm install -g snarkjs ``` ```plaintext pragma circom 2.0.0; template Multiplier() { signal input a; signal input b; signal output c; c <== a*b; } component main = Multiplier(); ``` ```plaintext circom myCircuit.circom --r1cs --wasm --symsnarkjs powersoftau new bn128 12 pot12_0000.ptau -vsnarkjs powersoftau contribute pot12_0000.ptau pot12_0001.ptau --name="First contribution" -vsnarkjs powersoftau prepare phase2 pot12_0001.ptau pot12_final.ptau -vsnarkjs groth16 setup myCircuit.r1cs pot12_final.ptau myCircuit_0000.zkeysnarkjs zkey contribute myCircuit_0000.zkey myCircuit_0001.zkey --name="1st Contributor Name" -vsnarkjs zkey export verificationkey myCircuit_0001.zkey verification_key.json ``` ```plaintext snarkjs zkey export solidityverifier myCircuit_0001.zkey verifier.sol ``` ```plaintext // SPDX-License-Identifier: MIT pragma solidity >=0.7.0 <0.9.0; // Interface that allows verifing proofs by sending them as a call to the verifier contract we just deployedinterface ICircomVerifier { function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[1] calldata _pubSignals) external view returns (bool);} // Contract that demonstrates the typical structure of a ZK verifer with custom logic in Soliditycontract CircomCustomLogic { ICircomVerifier circomVerifier; uint public publicInput; // Recieves a valid circom verifer contract address that was autogenerated by snarkjs constructor(address circomVeriferAddress) { circomVerifier = ICircomVerifier(circomVeriferAddress); } // Typical proof verifying function, execute custom logic if the proof passed as parameter is correct function sendProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[1] calldata _pubSignals) public { // ZK verification circomVerifier.verifyProof(_pA, _pB, _pC, _pubSignals); // Your custom logic, in this case just storing the multiplication result publicInput = _pubSignals[0]; }} ``` ```plaintext cd ..pnpm create wagmi zk-tutorial -t vite-reactcd frontendpnpm install snarkjs ``` ```plaintext zk-tutorial/├── public/│ └── zk_artifacts/│ ├── myCircuit.wasm│ ├── myCircuit_final.zkey│ └── verification_key.json├── src/│ ├── components/│ │ └── ZKForm.tsx│ └── App.tsx├── index.html├── tsconfig.json├── env.local└── package.json ``` ```plaintext VITE_COUNTER_CONTRACT_ADDRESS=0xYourNewContractAddress ``` ```plaintext import { useAccount, useWriteContract } from 'wagmi'import { createPublicClient, http } from 'viem'import { scrollSepolia } from 'viem/chains'import { useState, useEffect } from 'react'import { groth16 } from "snarkjs"; // Custom logic contract we just deployedconst CONTRACT_ADDRESS = import.meta.env.VITE_CIRCOM_CUSTOM_LOGIC_CONTRACT_ADDRESS as `0x${string}` // In this tutorial we'll use Scroll Sepolia, but this code can also be used on Scroll Mainnet or any EVM chain that supports all precompiles needed for ZK proofconst publicClient = createPublicClient({ chain: scrollSepolia, transport: http()}) // Custom logic contract ABI to be able to send proofs on-chainconst verifierAbi = [ { "inputs": [ { "internalType": "uint256[2]", "name": "_pA", "type": "uint256[2]" }, { "internalType": "uint256[2][2]", "name": "_pB", "type": "uint256[2][2]" }, { "internalType": "uint256[2]", "name": "_pC", "type": "uint256[2]" }, { "internalType": "uint256[1]", "name": "_pubSignals", "type": "uint256[1]" } ], "name": "sendProof", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "publicInput", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }] as const // Function responsible to generate private computation on the browser and then sending the computation proof on-chainfunction useZKForm() { const [a, setA] = useState("") const [b, setB] = useState("") const [message, setMessage] = useState("Connect your wallet") const [publicInput, setPublicInput] = useState(null) const { writeContract, isPending: isSendingProof } = useWriteContract() // Reads the contract state, in this case the multiplication result that was previously submmited const fetchPublicInput = async () => { try { const result = await publicClient.readContract({ address: CONTRACT_ADDRESS, abi: verifierAbi, functionName: "publicInput", }) setPublicInput(Number(result)) } catch (err) { console.error("Error fetching public input:", err) } } // Generates the ZK proof based on private parameters, and then sends the proofs to the smart contract on Scroll const sendProof = async () => { setMessage("Generating proof...") // Let's start by generating a proof by passing private inputs and the ZK artifacts generated by the circom compiler // Notice the ZK artifacts should be shared publicly in your website const { proof, publicSignals } = await groth16.fullProve( { a: Number(a), b: Number(b) }, "./zk_artifacts/myCircuit.wasm", "./zk_artifacts/myCircuit_final.zkey" ) // Now let's verify the proof locally, this is optional setMessage("Verifying off‑chain...") const vkey = await fetch("/verification_key.json").then((r) => r.json() ) const valid = await groth16.verify(vkey, publicSignals, proof) if (!valid) { setMessage("Proof verification failed") return } // We now translate the proof in a format compatible with what our smart contract expects const pA = proof.pi_a.slice(0, 2) // Take first two elements const pB = proof.pi_b.slice(0, 2).map((row: string[]) => row.slice(0, 2)) const pC = proof.pi_c.slice(0, 2) // Once the proof is ready we send it on-chain setMessage("Proof generated please confirm transaction.") writeContract({ address: CONTRACT_ADDRESS, abi: verifierAbi, functionName: "sendProof", args: [pA, pB, pC, publicSignals], }, { onSuccess: (txHash) => { setMessage("Executing...") publicClient.waitForTransactionReceipt({ hash: txHash }) .then(() => { setMessage("Success!") fetchPublicInput() }) .catch((err) => { console.error("ERROR! Transaction reverted:", err) setMessage("Transaction failed") }) }, onError: (err) => { console.error("ERROR! Transaction reverted:", err) setMessage("Transaction failed") } }) } return { a, setA, b, setB, message, publicInput, isSendingProof, sendProof, fetchPublicInput }} // React UI that handles contract reads, and private inputs for the ZK proof generationexport default function ZKForm() { const account = useAccount() const { a, setA, b, setB, message, publicInput, isSendingProof, sendProof, fetchPublicInput } = useZKForm() useEffect(() => { if (account.status === 'connected') { fetchPublicInput() } }, [account.status, fetchPublicInput]) return (

ZK Multiplication

{message}

setA(e.target.value)} /> setB(e.target.value)} /> {account.status === 'connected' && ( )} {publicInput !== null && (

Last stored result: {publicInput}

)}
)} ``` ```plaintext import { useAccount, useConnect, useDisconnect } from 'wagmi'// Import your ZKFormimport ZKForm from "./components/ZKForm"; function App() { const account = useAccount() const { connectors, connect, status, error } = useConnect() const { disconnect } = useDisconnect() return ( <>

Account

status: {account.status}
addresses: {JSON.stringify(account.addresses)}
chainId: {account.chainId}
{account.status === 'connected' && ( )}

Connect

{connectors.map((connector) => ( ))}
{status}
{error?.message}

ZK Tutorial

)} export default App ``` ```plaintext pnpm run dev ``` # Developer Quickstart | Scroll Documentation ## https://docs.scroll.io/en/developers/developer-quickstart/ Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer Developer Quickstart On This PageWhat You’ll BuildWhy Scroll?Install and Configure FoundryConfigure Environment VariablesDeploy Your Smart ContractCreate and Configure the React FrontendImplement the Frontend LogicStart the FrontendReady to go live? 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 Setting Up Your Wallet New to Web3? Start by installing Rabby or MetaMask. After creating your wallet and securing your seed phrase, head over to ChainList to add Scroll Sepolia to your wallet with a single click. Once you’re set up, you can export your private key for deployment by following MetaMask’s guide. Let’s set up both of these: .env SCROLL_SEPOLIA_RPC_URL="https://sepolia-rpc.scroll.io/"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. Security Warning Never share or commit your private key. Always keep it secure and handle with care. 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(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 (

Counter

Current number: {number?.toString()}

{account.status === 'connected' && ( )}
)} 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 (

Wallet

{account.status === 'connected' ? ( <>

Connected: {account.addresses?.[0]}

) : ( <> {connectors.map((connector) => ( ))} {error &&

{error.message}

} )}
{/* Use the Counter component in our dApp */}
)} 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 Verify Your Smart Contracts MoreEdit this pageJoin our community On This PageWhat You’ll BuildWhy Scroll?Install and Configure FoundryConfigure Environment VariablesDeploy Your Smart ContractCreate and Configure the React FrontendImplement the Frontend LogicStart the FrontendReady to go live? MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us ```plaintext # 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 ``` ```plaintext SCROLL_SEPOLIA_RPC_URL="https://sepolia-rpc.scroll.io/"PRIVATE_KEY= ``` ```plaintext .env ``` ```plaintext source .envforge create src/Counter.sol:Counter \ --rpc-url $SCROLL_SEPOLIA_RPC_URL \ --broadcast \ --private-key $PRIVATE_KEY ``` ```plaintext [⠊] Compiling...No files changed, compilation skippedDeployer: 0xbef34f2FCAe62dC3404c3d01AF65a7784c9c4A19Deployed to: 0xf0e9ceCAE516B2F5Ac297B3857453b07455f817FTransaction hash: 0x2bca5934ad82ce26332847fdd6a9241b0da0e38e6928f06499ee58ebc967bbde ``` ```plaintext cd ..pnpm create wagmi frontend -t vite-reactcd frontendpnpm install ``` ```plaintext VITE_COUNTER_CONTRACT_ADDRESS=0xYourNewContractAddress ``` ```plaintext 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(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 (

Counter

Current number: {number?.toString()}

{account.status === 'connected' && ( )}
)} ``` ```plaintext 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 (

Wallet

{account.status === 'connected' ? ( <>

Connected: {account.addresses?.[0]}

) : ( <> {connectors.map((connector) => ( ))} {error &&

{error.message}

} )}
{/* Use the Counter component in our dApp */}
)} export default App ``` ```plaintext pnpm run dev ``` ```plaintext import { scroll } from 'viem/chains' const publicClient = createPublicClient({ chain: scroll, transport: http(),}) ``` # Scroll Messenger Cross-chain Interaction | Scroll Documentation ## https://docs.scroll.io/en/developers/guides/scroll-messenger-cross-chain-interaction/ Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer Scroll Messenger Cross-chain Interaction On This PageDeploying the ContractsTarget Smart ContractOperator Smart ContractCalling a Cross-chain FunctionRelay the Message when sending from L2 to L1 In this example, we will launch a dummy smart contract on either Sepolia or Scroll and interact with it from the opposite chain. We will be using the ScrollMessenger that is deployed on both Sepolia and Scroll. Deploying the Contracts Target Smart Contract Let’s start by deploying the target smart contract. We will use the Greeter contract for this example, but you can use any other contract. Deploy it to either Sepolia or Scroll. On Scroll, L1 and L2 use the same API, so it’s up to you. // SPDX-License-Identifier: MITpragma solidity ^0.8.16; // This Greeter contract will be interacted with through the ScrollMessenger across the bridgecontract Greeter { string public greeting = "Hello World!"; // This function will be called by executeFunctionCrosschain on the Operator Smart Contract function setGreeting(string memory greeting_) public { greeting = greeting_; }} We will now execute setGreeting in a cross-chain way. Operator Smart Contract Switch to the other chain and deploy the GreeterOperator. So, if you deployed the Greeter contract on L1, deploy the GreeterOperator on L2 or vice versa. // SPDX-License-Identifier: MITpragma solidity ^0.8.16; // The Scroll Messenger interface is the same on both L1 and L2, it allows sending cross-chain transactions// Let's import it directly from the Scroll Contracts libraryimport "@scroll-tech/contracts@0.1.0/libraries/IScrollMessenger.sol"; // The GreeterOperator is capable of executing the Greeter function through the bridgecontract GreeterOperator { // This function will execute setGreeting on the Greeter contract function executeFunctionCrosschain( address scrollMessengerAddress, address targetAddress, uint256 value, string memory greeting, uint32 gasLimit ) public payable { IScrollMessenger scrollMessenger = IScrollMessenger(scrollMessengerAddress); // sendMessage is able to execute any function by encoding the abi using the encodeWithSignature function scrollMessenger.sendMessage{ value: msg.value }( targetAddress, value, abi.encodeWithSignature("setGreeting(string)", greeting), gasLimit, msg.sender ); }} Calling a Cross-chain Function We pass the message by executing executeFunctionCrosschain and passing the following parameters: scrollMessengerAddress: This will depend on where you deployed the GreeterOperator contract. If you deployed it on Sepolia use 0x50c7d3e7f7c656493D1D76aaa1a836CedfCBB16A. If you deployed on Scroll Sepolia use 0xBa50f5340FB9F3Bd074bD638c9BE13eCB36E603d. targetAddress: The address of the Greeter contract on the opposite chain. value: In this case, it is 0 because the setGreetingis not payable. greeting: This is the parameter that will be sent through the message. Try passing “This message was cross-chain!” gasLimit: If you are sending the message from L1 to L2, around 1000000 gas limit should be more than enough. That said, if you set this too high, and msg.value doesn’t cover gasLimit * baseFee, the transaction will revert. If msg.value is greater than the gas fee, the unused portion will be refunded. If you are sending the message from L2 to L1, pass 0, as the transaction will be completed by executing an additional transaction on L1. Relay the Message when sending from L2 to L1 When a transaction is passed from L2 to L1, an additional “execute withdrawal transaction” must be sent on L1. To do this, you must call relayMessageWithProof on the L1 Scroll Messenger contract from an EOA wallet. You can do this directly on Etherscan Sepolia. To do so, you will need to pass a Merkle inclusion proof for the bridged transaction and other parameters. You’ll query these using the Scroll Bridge API. We’re finalizing the API specifics, but for now, fetch or curl the following endpoint: curl "https://sepolia-api-bridge.scroll.io/api/claimable?page_size=10&page=1&address=GREETER_OPERATOR_ADDRESS_ON_L2" Replace GREETER_OPERATOR_ADDRESS_ON_L2 with your GreeterOperator contract address as launched on L2. Read more about Execute Withdraw transactions in the Scroll Messenger article. Experimental API This API was made for our Bridge UI. It is not yet finalized and may change in the future. We will update this guide when the API is finalized. Anyone can execute your L2 → L1 Message relayMessageWithProof is fully permissionless, so anyone can call it on your behalf if they’re willing to pay the L1 gas fees. This feature allows for additional support infrastructure, including tooling to automate this process for applications and users. After executing and confirming the transaction on both L1 and L2, the new state of greeting on the Greeter contract should be “This message was cross-chain!”. Sending a message from one chain to the other should take around 20 minutes after the transactions are confirmed on the origin chain. Congratulations, you now executed a transaction from one chain to the other using our native bridge! What's Next Bridge ERC20 through the Custom Gateway MoreEdit this pageJoin our community On This PageDeploying the ContractsTarget Smart ContractOperator Smart ContractCalling a Cross-chain FunctionRelay the Message when sending from L2 to L1 MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us ```plaintext // SPDX-License-Identifier: MITpragma solidity ^0.8.16; // This Greeter contract will be interacted with through the ScrollMessenger across the bridgecontract Greeter { string public greeting = "Hello World!"; // This function will be called by executeFunctionCrosschain on the Operator Smart Contract function setGreeting(string memory greeting_) public { greeting = greeting_; }} ``` ```plaintext // SPDX-License-Identifier: MITpragma solidity ^0.8.16; // The Scroll Messenger interface is the same on both L1 and L2, it allows sending cross-chain transactions// Let's import it directly from the Scroll Contracts libraryimport "@scroll-tech/contracts@0.1.0/libraries/IScrollMessenger.sol"; // The GreeterOperator is capable of executing the Greeter function through the bridgecontract GreeterOperator { // This function will execute setGreeting on the Greeter contract function executeFunctionCrosschain( address scrollMessengerAddress, address targetAddress, uint256 value, string memory greeting, uint32 gasLimit ) public payable { IScrollMessenger scrollMessenger = IScrollMessenger(scrollMessengerAddress); // sendMessage is able to execute any function by encoding the abi using the encodeWithSignature function scrollMessenger.sendMessage{ value: msg.value }( targetAddress, value, abi.encodeWithSignature("setGreeting(string)", greeting), gasLimit, msg.sender ); }} ``` ```plaintext curl "https://sepolia-api-bridge.scroll.io/api/claimable?page_size=10&page=1&address=GREETER_OPERATOR_ADDRESS_ON_L2" ``` # Contract Deployment Tutorial | Scroll Documentation ## https://docs.scroll.io/en/developers/guides/contract-deployment-tutorial/ Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer Contract Deployment Tutorial On This PageDeploy contracts with HardhatDeploy contracts with FoundryQuestions and Feedback The Scroll Sepolia Testnet allows anyone to deploy a smart contract on Scroll. In this tutorial, you will learn how to deploy a contract on Scroll Sepolia using common tools for developing on Ethereum. This demo repo illustrates contract deployment with Hardhat and Foundry. Got Testnet ETH? Before you start deploying the contract, you need to request test tokens from a Sepolia faucet and use the bridge to transfer some test ETH from Sepolia to Scroll Sepolia. Alternatively, you could acquire Scroll Sepolia ETH directly. See our Faucet and Bridge guides for help. Deploy contracts with Hardhat If you haven’t already, install nodejs and yarn. Clone the repo and install dependencies: git clone https://github.com/scroll-tech/scroll-guides.gitcd scroll-guides/contract-deploy-demoyarn install Create a .env file following the example .env.example in the root directory. Change PRIVATE_KEY to your own account private key in the .env. Run yarn compile to compile the contract. Run yarn deploy:scrollTestnet to deploy the contract on the Scroll Sepolia Testnet. Run yarn test for hardhat tests. Deploy contracts with Foundry Clone the repo: git clone https://github.com/scroll-tech/scroll-guides.gitcd scroll-guides/contract-deploy-demo Install Foundry: curl -L https://foundry.paradigm.xyz | bashfoundryup Run forge build to build the project. Deploy your contract with Foundry: forge create --rpc-url https://sepolia-rpc.scroll.io/ \ --value \ --constructor-args \ --private-key \ contracts/Lock.sol:Lock is the amount of test ETH to be locked in the contract. Try setting this to some small amount, like 0.0000001ether. is the Unix timestamp after which the funds locked in the contract will become available for withdrawal. Try setting this to some Unix timestamp in the future, like 1696118400 (this Unix timestamp corresponds to October 1, 2023). For example: forge create --rpc-url https://sepolia-rpc.scroll.io/ \ --value 0.00000000002ether \ --constructor-args 1696118400 \ --private-key 0xabc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc1 \ contracts/Lock.sol:Lock Questions and Feedback Thank you for participating in and developing on the Scroll Sepolia Testnet! If you encounter any issues, join our Discord and ask us in the #testnet-devs channel. What's Next Scroll Messenger Cross-chain Interaction MoreEdit this pageJoin our community On This PageDeploy contracts with HardhatDeploy contracts with FoundryQuestions and Feedback MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us ```plaintext git clone https://github.com/scroll-tech/scroll-guides.gitcd scroll-guides/contract-deploy-demoyarn install ``` ```plaintext git clone https://github.com/scroll-tech/scroll-guides.gitcd scroll-guides/contract-deploy-demo ``` ```plaintext curl -L https://foundry.paradigm.xyz | bashfoundryup ``` ```plaintext forge create --rpc-url https://sepolia-rpc.scroll.io/ \ --value \ --constructor-args \ --private-key \ contracts/Lock.sol:Lock ``` ```plaintext forge create --rpc-url https://sepolia-rpc.scroll.io/ \ --value 0.00000000002ether \ --constructor-args 1696118400 \ --private-key 0xabc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc1 \ contracts/Lock.sol:Lock ``` # Transaction Fees on Scroll | Scroll Documentation ## https://docs.scroll.io/en/developers/transaction-fees-on-scroll/ Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer Transaction Fees on Scroll On This PageOverviewL2 FeeCalculating the Execution FeeL1 FeeEstimating the L1 Data FeeCalculating the L1 Data Fee with Gas OracleL1GasPriceOracle APIL1 Originated TransactionsFuture RoadmapFootnotes Overview Scroll fees are notably lower than on its supporting layer. For users and developers, transaction fees on Scroll appear to work similarly to those on Ethereum mainnet, and existing tools, wallets, and code will likely work as if they were. However, the transaction fee shown in the wallet isn’t the whole picture unless the software is specifically aware of Scroll’s fee calculations. Due to the design of L2 rollups, transaction costs depend on the L1’s costs. To leverage Ethereum’s security, Scroll must account for the cost of the transaction data and proofs that must be stored and verified on the L1. Compared to Ethereum mainnet, Scroll introduces some new dimensions to transaction fee calculation to do this. The final cost of a transaction is constructed from several parts: L2 fee Calculated in the same manner as on the L1, equaling gas_price * gas_used L1 fee This additional fee covers sending data to L1 for data availability. Transactions initiated on the L1 do not pay this fee. It is calculated based on the size of the transaction’s calldata ETH is automatically deducted from the user’s balance on Scroll for the fee At a high level, the L2 fee is the cost of executing your transaction on the L2 sequencer, and the L1 fee is the cost of committing that transaction onto L1. In short, totalTxFee = l2Fee + l1Fee, all denominated in ETH, the native gas token for the Scroll network. Where are transaction fees sent? All tx fees are collected into the L2ScrollFeeVault contract balance. This contract also tracks the amount we’ve historically withdrawn to L1 using totalProcessed()(uint256).The block producer receives no direct reward, and the COINBASE opcode returns the fee vault address. L2 Fee Transactions on Scroll, like on Ethereum, must pay the cost of executing their computations and storing the data they produce. Calculating the Execution Fee The L2 execution fee is straightforward: l2TransactionExecutionFee = l2TransactionGasUsed * l2TransactionGasPrice The total fee depends on what the transaction does (l2TransactionGasUsed) as well as the current market conditions (l2TransactionGasPrice). Users set the gas price, and the “gas used” is assessed by calling the estimateGas endpoint on a Scroll node. In other words, the execution fee is calculated precisely like pre-EIP1559 Ethereum. L1 Fee Every transaction’s calldata must be committed to Ethereum, which incurs an additional transaction fee, referred to as the “L1 Fee”. Without doing this, Scroll couldn’t be reconstructed from only L1 data. Transactions aren’t committed 1-by-1 — they are collected in batches of blocks (and blocks of transactions). The cost of an individual transaction is computed based on the zeros and non-zero bytes in its payload. Estimating the L1 Data Fee Scroll has a pre-deployed L1GasPriceOracle at 0x5300000000000000000000000000000000000002, accessible on both Scroll Mainnet and Scroll Sepolia. It provides a getL1Fee method to estimate the L1 data fee for a given transaction’s raw data. function getL1Fee(bytes memory _data) external view override returns (uint256); What happens if gas prices spike on L1? Once the sequencer has processed a transaction, a user’s L1 fee is locked in, and any fluctuations will not affect what a user pays.Because of Scroll’s short block times, any L1 gas changes between a transaction’s submission and its inclusion in a block should be minimal. The sequencer will absorb any changes to L1 gas costs between a transaction’s inclusion in a block and when the sequencer commits the data to L1. Calculating the L1 Data Fee with Gas Oracle As mentioned, the L1GasPriceOracle is used to estimate the L1 gas fee given raw transaction data. This is a push oracle, updated by a relayer run by Scroll. The data fee is based on multiple factors: The bytes which are zeros and nonzeros of an RLP-encoded transaction with Signature l1BaseFee - Current base fee on the L1 overhead - Additional gas overhead of a data commitment transaction scalingFactor - A scaling factor used to account for price spikes PRECISION - A constant used to scale the final fee The following steps are taken to calculate the L1 data fee: Read three fields l1BaseFee, overhead, scalar from the L1GasPriceOracle contract. The slots for these fields in the contract are: FieldSlotl1BaseFee1overhead2scalar3 Count the number of zero bytes and non-zero bytes from the transaction. Calculate the L1 data fee, given TX_DATA_ZERO_GAS = 4 and TX_DATA_NON_ZERO_GAS = 161 and PRECISION = 1e9: l1Gas = zeros * TX_DATA_ZERO_GAS + (nonzeros + 4) * TX_DATA_NON_ZERO_GASl1GasFee = ((l1Gas + overhead) * l1BaseFee * scalar) / PRECISION We reserve an additional 4 bytes in the non-zero bytes to store the number of bytes in the RLP-encoded transaction. L1GasPriceOracle API overhead function overhead() external view returns (uint256); Returns the current L1 fee overhead scalar function scalar() external view returns (uint256); Returns the current l1 fee scalar l1BaseFee function l1BaseFee() external view returns (uint256); Returns the latest known l1 base fee getL1Fee function getL1Fee(bytes memory data) external view returns (uint256); Computes the L1 portion of the fee based on the size of the RLP encoded input transaction, the current L1 base fee, and the various dynamic parameters. Returns: L1 fee that should be paid for the transaction ParameterDescriptiondataSigned fully RLP-encoded transaction to get the L1 fee for. getL1GasUsed function getL1GasUsed(bytes memory data) external view returns (uint256); Computes the amount of L1 gas used for a transaction. Adds the overhead which represents the per-transaction gas overhead of posting the transaction and state roots to L1. Adds 74 bytes of padding to account for the fact that the input does not have a signature. Returns: Amount of L1 gas used to publish the transaction. ParameterDescriptiondataSigned fully RLP-encoded transaction to get the L1 fee for. L1 Originated Transactions When messaging from L1 to L2, the user pays all transaction fees on L1. The user pays L1 gas fees, but because of this, doesn’t need to pay Scroll an L1 Data Fee. They will need to account for L2 Execution Fees in their L1 transaction though, and will need to know how much L2 gas to pay. Contracts on L1 can use an L2 Gas Price Oracle deployed to the L1 to get the gas fee for a given transaction. The oracle also exposes the current l2BaseFee and estimated cross-domain message fee for a given gas limit. On mainnet, the L2GasPriceOracle is deployed at 0x987e300fDfb06093859358522a79098848C33852. On Sepolia, an upgraded L1MessageQueueWithGasPriceOracle should be used, deployed to 0xF0B2293F5D834eAe920c6974D50957A1732de763. If your system supports off-chain mechanisms, you can also call eth_estimateGas and eth_gasPrice on any Scroll RPC node to get an estimate of the gas required for a given transaction. Upgrade Notice After the February 2024 Bridge Upgrade, L2GasPriceOracle will be deprecated in favor of L1MessageQueueWithGasPriceOracle, which will be available at 0x0d7E906BD9cAFa154b048cFa766Cc1E54E39AF9B.The upgrade is expected to be finalized on February 21, 2024, after a two-week timelock. Scroll Sepolia has already undergone this upgrade. Read more here. Future Roadmap Currently, the computation required for proof generation and gas for proof verification on L1 is subsidized by Scroll and various proving partners. It is partially covered by setting a floor to L2 Gas Price. As the prover network becomes decentralized, incentives for proof generation will need to be incorporated into the protocol for the system to be sustainable and scalable. We expect the final gas cost will explicitly include the cost of this proof generation. With further protocol optimization, this cost for the user should be minimal, as each proof covers many transactions. Footnotes See EIP-2028 for more info. ↩ What's Next Contract Deployment Tutorial MoreEdit this pageJoin our community On This PageOverviewL2 FeeCalculating the Execution FeeL1 FeeEstimating the L1 Data FeeCalculating the L1 Data Fee with Gas OracleL1GasPriceOracle APIL1 Originated TransactionsFuture RoadmapFootnotes MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us | Field | Slot | || --- || --- || | l1BaseFee | 1 | | overhead | 2 | | scalar | 3 | | Parameter | Description | || --- || --- || | data | Signed fully RLP-encoded transaction to get the L1 fee for. | | Parameter | Description | || --- || --- || | data | Signed fully RLP-encoded transaction to get the L1 fee for. | ```plaintext l2TransactionExecutionFee = l2TransactionGasUsed * l2TransactionGasPrice ``` ```plaintext function getL1Fee(bytes memory _data) external view override returns (uint256); ``` ```plaintext l1Gas = zeros * TX_DATA_ZERO_GAS + (nonzeros + 4) * TX_DATA_NON_ZERO_GASl1GasFee = ((l1Gas + overhead) * l1BaseFee * scalar) / PRECISION ``` ```plaintext function overhead() external view returns (uint256); ``` ```plaintext function scalar() external view returns (uint256); ``` ```plaintext function l1BaseFee() external view returns (uint256); ``` ```plaintext function getL1Fee(bytes memory data) external view returns (uint256); ``` ```plaintext function getL1GasUsed(bytes memory data) external view returns (uint256); ``` # The Scroll Messenger | Scroll Documentation ## https://docs.scroll.io/en/developers/l1-and-l2-bridging/the-scroll-messenger/ Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer The Scroll Messenger On This PageFinalizing transactions on L1Messenger APIsendMessagerelayMessageWithProof The Scroll Messenger contracts allow for sending arbitrary messages from L1 to L2 or vice versa. This enables executing functions on another chain in a secure and permissionless way. To send a message from L1 to L2, use the messenger smart contract deployed on L1, L1ScrollMessenger. To send a message from L2 to L1, use the contract deployed on L2, L2ScrollMessenger. When sending a transaction through the Scroll Messenger deployed on L1 and L2, the resulting transaction sender (CALLER or msg.sender) will be the Messenger Contract address deployed on the receiving chain.In future Scroll versions, enforced transactions from the L1 will allow setting the sender on L2 as the original EOA on L1. It will also allow 3rd parties to relay signed transactions securely. Finalizing transactions on L1 Any upcoming transactions from L2 need to be finalized using the relayMessageWithProof function on the Scroll Messenger contract. We call this process “submitting an Execute Withdrawal transaction,” and it is required for both sending arbitrary messages and transferring assets through a gateway or the router. When you use relayMessageWithProof, you’ll have to provide a Merkle inclusion proof showing your transaction is included in the trie of “withdrawal” messages, along with other parameters. Producing this proof and these values can be done locally and permissionlessly, but at the moment, the easiest way to retrieve these parameters is through our backend APIs: Scroll Sepolia API: https://sepolia-api-bridge-v2.scroll.io/api/ Scroll API: https://mainnet-api-bridge-v2.scroll.io/api/ Experimental API This API was made for our Bridge UI. It is not yet finalized and may change in the future. We will update this guide when the API is finalized. Additionally, all examples below use the Sepolia API service — the calls should be easily adapted to work on mainnet. Supply the address of the EOA or contract responsible for initiating the original transaction on L2 to the /claimable endpoint. The API backend will provide you with all the necessary information to successfully conclude the transaction on L1. Take a look at the following example: https://sepolia-api-bridge.scroll.io/api/claimable?address=0x031407eaaffFB4f1EC2c999bE4477E52C81de3c5&page_size=10&page=1 The API should return your transaction data in the following format: { "errcode": 0, "errmsg": "", "data": { "result": [ { "hash": "0xa476850306d6ee52b127628ded34dcf2343570873cce9c5383bd497db48d4f9b", "amount": "", "to": "", "isL1": false, "l1Token": "", "l2Token": "", "blockNumber": 748, "blockTimestamp": null, "finalizeTx": { "hash": "", "amount": "", "to": "", "isL1": false, "blockNumber": 0, "blockTimestamp": null }, "claimInfo": { "from": "0x031407eaaffFB4f1EC2c999bE4477E52C81de3c5", "to": "0x1039057185CFe192d16c03F5656225821A193FD5", "value": "0", "nonce": "9", "batch_hash": "0x49a18d72dbceeb957f918947b532db452c031f528e7e6bf329007066638c5e50", "message": "0xa413686200000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000005686f6c6973000000000000000000000000000000000000000000000000000000", "proof": "0x69b4ee6cf9a38bed79668ddd347fef2bdff44c3760c9309fa41decfd60202d22ad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5b4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d3079f53171df5c0661d2afe86c4d97b6f34278daf6a0ea9baff5b4fc979d5629a5", "batch_index": "93" }, "createdTime": null } ], "total": 1 }} The claimInfo object under the result json returned has all the information needed to execute your transaction on L1. The parameters needed by the relayMessageWithProof are: from, to, value, nonce, message and proof. Supply these to the relayMessageWithProof function on L1 to execute and finalize your transaction on L1. All L2 transactions are bundled into batches – you have to wait for the batch that includes your transaction to finalize before calling relayMessageWithProof. Your transaction batch index is returned in the batch_index value on the /claimable endpoint, and you can follow the progress on the Scroll Rollup Explorer. Messenger API Please visit the npm library for the complete Scroll contract API documentation. sendMessage function sendMessage( address target, uint256 value, bytes calldata message, uint256 gasLimit, address refundAddress) external payable; Sends arbitrary data from one chain to another. It allows us to execute functions cross-chain. ParameterDescriptiontargetThe address of the account that receives the message. The receiver can be either a smart contract or an EOA wallet.valueThe amount of ether passed when calling the target contract.messageThe content of the message. This is the arbitrary calldata to be executed.gasLimitGas limit required to complete the message relay on the corresponding chain.refundAddressThe address of the account that will receive the refunded fee. relayMessageWithProof function relayMessageWithProof( address from, address to, uint256 value, uint256 nonce, bytes memory message, L2MessageProof memory proof) external; Relay a L2 => L1 message with message proof. ParameterDescriptionfromThe address of the sender of the message.toThe address of the recipient of the message.valueThe msg.value passed to the message call.nonceThe nonce of the message to avoid replay attack.messageThe content of the message.proofThe proof used to verify the correctness of the transaction. What's Next Transaction Fees on Scroll MoreEdit this pageJoin our community On This PageFinalizing transactions on L1Messenger APIsendMessagerelayMessageWithProof MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us | Parameter | Description | || --- || --- || | target | The address of the account that receives the message. The receiver can be either a smart contract or an EOA wallet. | | value | The amount of ether passed when calling the target contract. | | message | The content of the message. This is the arbitrary calldata to be executed. | | gasLimit | Gas limit required to complete the message relay on the corresponding chain. | | refundAddress | The address of the account that will receive the refunded fee. | | Parameter | Description | || --- || --- || | from | The address of the sender of the message. | | to | The address of the recipient of the message. | | value | The msg.value passed to the message call. | | nonce | The nonce of the message to avoid replay attack. | | message | The content of the message. | | proof | The proof used to verify the correctness of the transaction. | ```plaintext https://sepolia-api-bridge.scroll.io/api/claimable?address=0x031407eaaffFB4f1EC2c999bE4477E52C81de3c5&page_size=10&page=1 ``` ```plaintext { "errcode": 0, "errmsg": "", "data": { "result": [ { "hash": "0xa476850306d6ee52b127628ded34dcf2343570873cce9c5383bd497db48d4f9b", "amount": "", "to": "", "isL1": false, "l1Token": "", "l2Token": "", "blockNumber": 748, "blockTimestamp": null, "finalizeTx": { "hash": "", "amount": "", "to": "", "isL1": false, "blockNumber": 0, "blockTimestamp": null }, "claimInfo": { "from": "0x031407eaaffFB4f1EC2c999bE4477E52C81de3c5", "to": "0x1039057185CFe192d16c03F5656225821A193FD5", "value": "0", "nonce": "9", "batch_hash": "0x49a18d72dbceeb957f918947b532db452c031f528e7e6bf329007066638c5e50", "message": "0xa413686200000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000005686f6c6973000000000000000000000000000000000000000000000000000000", "proof": "0x69b4ee6cf9a38bed79668ddd347fef2bdff44c3760c9309fa41decfd60202d22ad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5b4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d3079f53171df5c0661d2afe86c4d97b6f34278daf6a0ea9baff5b4fc979d5629a5", "batch_index": "93" }, "createdTime": null } ], "total": 1 }} ``` ```plaintext function sendMessage( address target, uint256 value, bytes calldata message, uint256 gasLimit, address refundAddress) external payable; ``` ```plaintext function relayMessageWithProof( address from, address to, uint256 value, uint256 nonce, bytes memory message, L2MessageProof memory proof) external; ``` # ERC1155 Token Bridge | Scroll Documentation ## https://docs.scroll.io/en/developers/l1-and-l2-bridging/erc1155-token-bridge/ Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer ERC1155 Token Bridge On This PageDeposit ERC1155 tokens from L1Creating an ERC1155 token on L2Adding an ERC1155 token to the Scroll BridgeWithdraw ERC1155 tokens from L2L1ERC1155Gateway APIdepositERC1155updateTokenMappingL2ERC1155Gateway APIwithdrawERC1155updateTokenMapping Deposit ERC1155 tokens from L1 ERC1155 bridging from L1 to L2 is done via the L1ERC1155Gateway. Similarly to ERC721 bridging, we don’t use a router but the depositERC1155 function on the Gateway directly. depositERC1155 is a payable function, and the amount of ETH sent to this function will be used to pay for L2 fees. If the amount is not enough, the transaction will not be sent. All excess eth will be sent back to the sender. 0.00001 ETH should be more than enough to process a token deposit. Creating an ERC1155 token on L2 Similar to ERC721 bridging, in order to bridge ERC1155 tokens, a contract compatible with the IScrollERC1155 standard has to be launched and mapped on a L1ERC1155Gateway and L2ERC1155Gateway on both L1 and L2 respectively. This contract has to grant permission to the Gateway on L2 to mint when a token is deposited and burn when the token is withdrawn. The following interface is the IScrollERC1155 needed for deploying ERC1155 tokens compatible with the L2ERC1155Gateway on L2. interface IScrollERC1155 { /// @notice Return the address of Gateway the token belongs to. function gateway() external view returns (address); /// @notice Return the address of counterpart token. function counterpart() external view returns (address); /// @notice Mint some token to recipient's account. /// @dev Gateway Utilities, only gateway contract can call /// @param _to The address of recipient. /// @param _tokenId The token id to mint. /// @param _amount The amount of token to mint. /// @param _data The data passed to recipient function mint(address _to, uint256 _tokenId, uint256 _amount, bytes memory _data) external; /// @notice Burn some token from account. /// @dev Gateway Utilities, only gateway contract can call /// @param _from The address of account to burn token. /// @param _tokenId The token id to burn. /// @param _amount The amount of token to burn. function burn(address _from, uint256 _tokenId, uint256 _amount) external; /// @notice Batch mint some token to recipient's account. /// @dev Gateway Utilities, only gateway contract can call /// @param _to The address of recipient. /// @param _tokenIds The list of token ids to mint. /// @param _amounts The list of corresponding amount of token to mint. /// @param _data The data passed to recipient function batchMint( address _to, uint256[] calldata _tokenIds, uint256[] calldata _amounts, bytes calldata _data ) external; /// @notice Batch burn some token from account. /// @dev Gateway Utilities, only gateway contract can call /// @param _from The address of account to burn token. /// @param _tokenIds The list of token ids to burn. /// @param _amounts The list of corresponding amount of token to burn. function batchBurn(address _from, uint256[] calldata _tokenIds, uint256[] calldata _amounts) external;} Adding an ERC1155 token to the Scroll Bridge All assets can be bridged securely and permissionlessly through Gateway contracts deployed by any developer. However, Scroll also manages an ERC1155 Gateway contract where all NFTs created by the community are welcome. Being part of the Scroll-managed Gateway means you won’t need to deploy the Gateway contracts, and your token will appear in the Scroll frontend. To be part of the Scroll Gateway, you must contact the Scroll team to add the token to both L1 and L2 Gateway contracts. To do so, follow the instructions on the token lists repository to add your token to the Scroll official frontend. Withdraw ERC1155 tokens from L2 The L2ERC1155Gateway contract is used to send tokens from L2 to L1. Before bridging, the L2ERC1155Gateway contract has to be approved by the token contract. Once that is done, withdrawERC1155 can be called to perform the asset bridge. withdrawERC1155 is a payable function, and the amount of ETH sent to this function will be used to pay for L1 fees. If the amount is not enough, the transaction will not be sent. All excess ETH will be sent back to sender. Fees depend on L1 activity but 0.005 ETH should be enough to process a token withdrawal. Make sure the transaction won’t revert on L1 while sending from L2. There is no way to recover tokens bridged if you’re transaction reverts on L1. All assets are burnt on L2 when the transaction is sent, and it’s impossible to mint them again. L1ERC1155Gateway API Please visit the npm library for the complete Scroll contract API documentation. depositERC1155 function depositERC1155( address _token, address _to, uint256 _tokenId, uint256 _amount, uint256 _gasLimit) external payable; Deposit an ERC1155 token from L1 to a recipient’s account on L2. ParameterDescriptiontokenThe address of ERC1155 token contract on L1.toThe address of recipient’s account on L2.tokenIdThe NFT id to deposit.amountThe amount of tokens to deposit.gasLimitGas limit required to complete the deposit on L2. Unused portion of fee will be refunded. updateTokenMapping function updateTokenMapping(address _l1Token, address _l2Token) external; Update the mapping that connects an ERC1155 token contract from L1 to L2. ParameterDescription_l1TokenThe address of the ERC1155 token in L1._l2TokenThe address of corresponding ERC1155 token in L2. L2ERC1155Gateway API withdrawERC1155 function withdrawERC1155(address token, address to, uint256 tokenId, uint256 amount, uint256 gasLimit) external payable; Send ERC1155 tokens from L2 to a recipient’s account on L1. ParameterDescriptiontokenThe address of ERC1155 token contract on L2.toThe address of recipient’s account on L1.tokenIdThe NFT id to withdraw.amountThe amount of tokens to withdraw.gasLimitUnused, but included for potential forward compatibility considerations. updateTokenMapping function updateTokenMapping(address _l1Token, address _l2Token) external; Update the mapping that connects an ERC1155 token contract from L2 to L1. ParameterDescription_l1TokenThe address of the ERC1155 token in L1._l2TokenThe address of corresponding ERC1155 token in L2. What's Next The Scroll Messenger MoreEdit this pageJoin our community On This PageDeposit ERC1155 tokens from L1Creating an ERC1155 token on L2Adding an ERC1155 token to the Scroll BridgeWithdraw ERC1155 tokens from L2L1ERC1155Gateway APIdepositERC1155updateTokenMappingL2ERC1155Gateway APIwithdrawERC1155updateTokenMapping MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us | Parameter | Description | || --- || --- || | token | The address of ERC1155 token contract on L1. | | to | The address of recipient’s account on L2. | | tokenId | The NFT id to deposit. | | amount | The amount of tokens to deposit. | | gasLimit | Gas limit required to complete the deposit on L2. Unused portion of fee will be refunded. | | Parameter | Description | || --- || --- || | _l1Token | The address of the ERC1155 token in L1. | | _l2Token | The address of corresponding ERC1155 token in L2. | | Parameter | Description | || --- || --- || | token | The address of ERC1155 token contract on L2. | | to | The address of recipient’s account on L1. | | tokenId | The NFT id to withdraw. | | amount | The amount of tokens to withdraw. | | gasLimit | Unused, but included for potential forward compatibility considerations. | | Parameter | Description | || --- || --- || | _l1Token | The address of the ERC1155 token in L1. | | _l2Token | The address of corresponding ERC1155 token in L2. | ```plaintext interface IScrollERC1155 { /// @notice Return the address of Gateway the token belongs to. function gateway() external view returns (address); /// @notice Return the address of counterpart token. function counterpart() external view returns (address); /// @notice Mint some token to recipient's account. /// @dev Gateway Utilities, only gateway contract can call /// @param _to The address of recipient. /// @param _tokenId The token id to mint. /// @param _amount The amount of token to mint. /// @param _data The data passed to recipient function mint(address _to, uint256 _tokenId, uint256 _amount, bytes memory _data) external; /// @notice Burn some token from account. /// @dev Gateway Utilities, only gateway contract can call /// @param _from The address of account to burn token. /// @param _tokenId The token id to burn. /// @param _amount The amount of token to burn. function burn(address _from, uint256 _tokenId, uint256 _amount) external; /// @notice Batch mint some token to recipient's account. /// @dev Gateway Utilities, only gateway contract can call /// @param _to The address of recipient. /// @param _tokenIds The list of token ids to mint. /// @param _amounts The list of corresponding amount of token to mint. /// @param _data The data passed to recipient function batchMint( address _to, uint256[] calldata _tokenIds, uint256[] calldata _amounts, bytes calldata _data ) external; /// @notice Batch burn some token from account. /// @dev Gateway Utilities, only gateway contract can call /// @param _from The address of account to burn token. /// @param _tokenIds The list of token ids to burn. /// @param _amounts The list of corresponding amount of token to burn. function batchBurn(address _from, uint256[] calldata _tokenIds, uint256[] calldata _amounts) external;} ``` ```plaintext function depositERC1155( address _token, address _to, uint256 _tokenId, uint256 _amount, uint256 _gasLimit) external payable; ``` ```plaintext function updateTokenMapping(address _l1Token, address _l2Token) external; ``` ```plaintext function withdrawERC1155(address token, address to, uint256 tokenId, uint256 amount, uint256 gasLimit) external payable; ``` ```plaintext function updateTokenMapping(address _l1Token, address _l2Token) external; ``` # ERC721 NFT Bridge | Scroll Documentation ## https://docs.scroll.io/en/developers/l1-and-l2-bridging/erc721-nft-bridge/ Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer ERC721 NFT Bridge On This PageDeposit ERC721 tokens from L1Creating a ScrollERC721 token on L2Adding ERC721 NFTs to the Scroll BridgeWithdraw ERC721 tokens from ScrollL1ERC721Gateway APIdepositERC721updateTokenMappingL2ERC721Gateway APIwithdrawERC721updateTokenMapping Deposit ERC721 tokens from L1 NFT bridging from L1 to L2 is done via the L1ERC721Gateway contract instead of using a router. Similarly to bridging ERC20 tokens, we use the depositERC721 function to send tokens to L2, and we can later retrieve them back to L1 using withdrawERC721 on the L2ERC721Gateway contract deployed on L2. NFT contracts on both L1 and l2 must be launched and connected through the Gateways to enable bridging. This means deposit and withdraw transactions will revert if a contract on either L1 or L2 is missing or not mapped through the updateTokenMapping function. depositERC721 is a payable function. The amount of ETH sent to this function will be used to pay for L2 fees. If the amount is not enough, the transaction will not be sent. All excess eth will be sent back to the sender. 0.00001 ETH should be more than enough to process a token deposit. Creating a ScrollERC721 token on L2 To deposit an ERC721 token to L2, a token contract compatible with the IScrollERC721 standard has to be launched and mapped on a L1ERC721Gateway and L2ERC721Gateway on both L1 and L2, respectively. This contract has to grant permission to the Gateway on L2 to mint when a token is deposited and burn when the token is withdrawn. The following interface is the IScrollERC721 needed for deploying ERC721 tokens compatible with the L2ERC721Gateway on L2. interface IScrollERC721 { /// @notice Return the address of Gateway the token belongs to. function gateway() external view returns (address); /// @notice Return the address of counterpart token. function counterpart() external view returns (address); /// @notice Mint some token to recipient's account. /// @dev Gateway Utilities, only gateway contract can call /// @param _to The address of recipient. /// @param _tokenId The token id to mint. function mint(address _to, uint256 _tokenId) external; /// @notice Burn some token from account. /// @dev Gateway Utilities, only gateway contract can call /// @param _tokenId The token id to burn. function burn(uint256 _tokenId) external;} Adding ERC721 NFTs to the Scroll Bridge All assets can be bridged securely and permissionlessly through Gateway contracts deployed by any developer. However, Scroll also manages an ERC721 Gateway contract where all NFTs created by the community are welcome. Being part of the Scroll-managed Gateway means you won’t need to deploy the Gateway contracts, and your token will appear in the Scroll frontend. To be part of the Scroll Gateway, you must contact the Scroll team to add the NFT on both L1 and L2 Gateway contracts. To do so, follow the instructions on the token lists repository to add a new token to the Scroll official frontend. Withdraw ERC721 tokens from Scroll The L2ERC721Gateway contract is used to send tokens from L2 to L1. Before bridging, the L2ERC721Gateway contract has to be approved by the NFT contract. Once that is done, withdrawERC721 can be called to perform the asset bridge. withdrawERC721 is a payable function, and the amount of ETH sent to this function will be used to pay for L1 fees. If the amount is not enough, the transaction will not be sent. All excess ETH will be sent back to sender. Fees depend on L1 activity but 0.005 ETH should be enough to process a token withdrawal. Make sure the transaction won’t revert on L1 while sending from L2. There is no way to recover the NFT bridged if your transaction reverts on L1. All assets are burnt on L2 when the transaction is sent, and it’s impossible to mint them again. L1ERC721Gateway API Please visit the npm library for the complete Scroll contract API documentation. depositERC721 function depositERC721(address _token, address _to, uint256 _tokenId, uint256 _gasLimit) external payable; Deposit an ERC721 NFT from L1 to a recipient’s account on L2. ParameterDescriptiontokenThe address of ERC721 NFT contract on L1.toThe address of recipient’s account on L2.tokenIdThe NFT id to deposit.gasLimitGas limit required to complete the deposit on L2. Unused portion of fee will be refunded. updateTokenMapping function updateTokenMapping(address _l1Token, address _l2Token) external; Update the mapping that connects an NFT contract from L1 to L2. ParameterDescription_l1TokenThe address of ERC721 token in L1._l2TokenThe address of corresponding ERC721 token in L2. L2ERC721Gateway API withdrawERC721 function withdrawERC721(address _token, address _to, uint256 _tokenId, uint256 _gasLimit) external payable; Send an ERC721 NFT from L2 to a recipient’s account on L1. ParameterDescriptiontokenThe address of ERC721 NFT token contract on L2.toThe address of recipient’s account on L1.tokenIdThe NFT id to deposit.gasLimitUnused, but included for potential forward compatibility considerations. updateTokenMapping function updateTokenMapping(address _l1Token, address _l2Token) external; Update the mapping that connects an NFT contract from L2 to L1. ParameterDescription_l2TokenThe address of ERC721 token in L2._l1TokenThe address of corresponding ERC721 token in L1. What's Next ERC1155 Token Bridge MoreEdit this pageJoin our community On This PageDeposit ERC721 tokens from L1Creating a ScrollERC721 token on L2Adding ERC721 NFTs to the Scroll BridgeWithdraw ERC721 tokens from ScrollL1ERC721Gateway APIdepositERC721updateTokenMappingL2ERC721Gateway APIwithdrawERC721updateTokenMapping MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us | Parameter | Description | || --- || --- || | token | The address of ERC721 NFT contract on L1. | | to | The address of recipient’s account on L2. | | tokenId | The NFT id to deposit. | | gasLimit | Gas limit required to complete the deposit on L2. Unused portion of fee will be refunded. | | Parameter | Description | || --- || --- || | _l1Token | The address of ERC721 token in L1. | | _l2Token | The address of corresponding ERC721 token in L2. | | Parameter | Description | || --- || --- || | token | The address of ERC721 NFT token contract on L2. | | to | The address of recipient’s account on L1. | | tokenId | The NFT id to deposit. | | gasLimit | Unused, but included for potential forward compatibility considerations. | | Parameter | Description | || --- || --- || | _l2Token | The address of ERC721 token in L2. | | _l1Token | The address of corresponding ERC721 token in L1. | ```plaintext interface IScrollERC721 { /// @notice Return the address of Gateway the token belongs to. function gateway() external view returns (address); /// @notice Return the address of counterpart token. function counterpart() external view returns (address); /// @notice Mint some token to recipient's account. /// @dev Gateway Utilities, only gateway contract can call /// @param _to The address of recipient. /// @param _tokenId The token id to mint. function mint(address _to, uint256 _tokenId) external; /// @notice Burn some token from account. /// @dev Gateway Utilities, only gateway contract can call /// @param _tokenId The token id to burn. function burn(uint256 _tokenId) external;} ``` ```plaintext function depositERC721(address _token, address _to, uint256 _tokenId, uint256 _gasLimit) external payable; ``` ```plaintext function updateTokenMapping(address _l1Token, address _l2Token) external; ``` ```plaintext function withdrawERC721(address _token, address _to, uint256 _tokenId, uint256 _gasLimit) external payable; ``` ```plaintext function updateTokenMapping(address _l1Token, address _l2Token) external; ``` # ETH and ERC20 Token Bridge | Scroll Documentation ## https://docs.scroll.io/en/developers/l1-and-l2-bridging/eth-and-erc20-token-bridge/ Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer ETH and ERC20 Token Bridge On This PageDeposit ETH and ERC20 tokens from L1Withdraw ETH and ERC20 tokens from L2Creating an ERC20 token with custom logic on L2Adding a Custom L2 ERC20 token to the Scroll BridgeL1 Gateway APIdepositETHdepositERC20getL2ERC20AddressupdateTokenMappingL2 Gateway APIwithdrawETHwithdrawERC20getL1ERC20AddressupdateTokenMapping Deposit ETH and ERC20 tokens from L1 The Gateway Router allows ETH and ERC20 token bridging from L1 to L2 using the depositETH and depositERC20 functions respectively. It is a permissionless bridge deployed on L1. Notice that ERC20 tokens will have a different address on L2, you can use the getL2ERC20Address function to query the new address. depositETH and depositERC20 are payable functions, the amount of ETH sent to these functions will be used to pay for L2 fees. If the amount is not enough, the transaction will not be sent. All excess ETH will be sent back to the sender. 0.00001 ETH should be more than enough to process a token deposit. When bridging ERC20 tokens, you don’t have to worry about selecting the right Gateway. This is because the L1GatewayRouter will choose the correct underlying entry point to send the message: L1StandardERC20Gateway: This Gateway permits any ERC20 deposit and will be selected as the default by the L1GatewayRouter for an ERC20 token that doesn’t need custom logic on L2. On the very first token bridging, a new token will be created on L2 that implements the ScrollStandardERC20. To bridge a token, call the depositERC20 function on the L1GatewayRouter. L1CustomERC20Gateway: This Gateway will be selected by the L1GatewayRouter for tokens with custom logic. For an L1/L2 token pair to work on the Scroll Custom ERC20 Bridge, the L2 token contract has to implement IScrollStandardERC20. Additionally, the token should grant mint or burn capability to the L2CustomERC20Gateway. Visit the Bridge an ERC20 through the Custom Gateway guide for a step-by-step example of how to bridge a custom token. All Gateway contracts will form the message and send it to the L1ScrollMessenger which can send arbitrary messages to L2. The L1ScrollMessenger passes the message to the L1MessageQueue. Any user can send messages directly to the Messenger to execute arbitrary data on L2. This means they can execute any function on L2 from a transaction made on L1 via the bridge. Although an application could directly pass messages to existing token contracts, the Gateway abstracts the specifics and simplifies making transfers and calls. In future upgrades, users will be able to bypass the L1ScrollMessenger and send messages directly to the L1MessageQueue. If a message is sent via the L1MessageQueue, the transaction’s sender will be the address of the user sending the transaction, not the address of the L1ScrollMessenger. When a new block gets created on L1, the Watcher will detect the message on the L1MessageQueue and will pass it to the Relayer service, which will submit the transaction to the L2 via the l2geth node. Finally, the l2geth node will pass the transaction to the L2ScrollMessenger contract for execution on L2. Withdraw ETH and ERC20 tokens from L2 The L2 Gateway is very similar to the L1 Gateway. We can withdraw ETH and ERC20 tokens back to L1 using the withdrawETH and withdrawERC20 functions. The contract address is deployed on L2. We use the getL1ERC20Address to retrieve the token address on L1. withdrawETH and withdrawERC20 are payable functions, and the amount of ETH sent to these functions will be used to pay for L1 fees. If the amount is not enough, the transaction will not be sent. All excess ETH will be sent back to the sender. Fees will depend on L1 activity but 0.005 ETH should be enough to process a token withdrawal. Make sure the transactions won’t revert on L1 while sending from L2. There is no way to recover bridged ETH, tokens, or NFTs if your transaction reverts on L1. All assets are burnt on Scroll when the transaction is sent, and it’s impossible to mint them again. Creating an ERC20 token with custom logic on L2 If a token needs custom logic on L2, it will need to be bridged through an L1CustomERC20Gateway and L2CustomERC20Gateway respectively. The custom token on L2 will need to give permission to the Gateway to mint new tokens when a deposit occurs and to burn when tokens are withdrawn The following interface is the IScrollStandardERC20 needed for deploying tokens compatible with the L2CustomERC20Gateway on L2. interface IScrollStandardERC20 { /// @notice Return the address of Gateway the token belongs to. function gateway() external view returns (address); /// @notice Return the address of counterpart token. function counterpart() external view returns (address); /// @dev ERC677 Standard, see https://github.com/ethereum/EIPs/issues/677 /// Defi can use this method to transfer L1/L2 token to L2/L1, /// and deposit to L2/L1 contract in one transaction function transferAndCall(address receiver, uint256 amount, bytes calldata data) external returns (bool success); /// @notice Mint some token to recipient's account. /// @dev Gateway Utilities, only gateway contract can call /// @param _to The address of recipient. /// @param _amount The amount of token to mint. function mint(address _to, uint256 _amount) external; /// @notice Burn some token from account. /// @dev Gateway Utilities, only gateway contract can call /// @param _from The address of account to burn token. /// @param _amount The amount of token to mint. function burn(address _from, uint256 _amount) external;} Adding a Custom L2 ERC20 token to the Scroll Bridge Tokens can be bridged securely and permissionlessly through Gateway contracts deployed by any developer. However, Scroll also manages an ERC20 Router and a Gateway where all tokens created by the community are welcome. Being part of the Scroll-managed Gateway means you won’t need to deploy the Gateway contracts, and your token will appear in the Scroll frontend. To be part of the Scroll Gateway, you must contact the Scroll team to add the token to both L1 and L2 bridge contracts. To do so, follow the instructions on the token lists repository to add your new token to the official Scroll frontend. L1 Gateway API Please visit the npm library for the complete Scroll contract API documentation. depositETH function depositETH(address _to, uint256 _amount, uint256 _gasLimit) public payable; Sends ETH from L1 to L2. ParameterDescriptiontoThe address of recipient’s account on L2.amountThe amount of token to transfer, in wei.gasLimitGas limit required to complete the deposit on L2. 170000 should be enough to process the transaction, but unused funds are refunded. depositERC20 function depositERC20(address _token, address _to, uint256 _amount, uint256 _gasLimit) payable; Sends ERC20 tokens from L1 to L2. ParameterDescriptiontokenThe token address on L1.toThe address of recipient’s account on L2.amountThe amount of token to transfer, in wei.gasLimitGas limit required to complete the deposit on L2. 200000 should be enough to process the transaction, depending on the Gateway, but unused funds are refunded. getL2ERC20Address function getL2ERC20Address(address _l1Token) external view returns (address); Returns the corresponding L2 token address given L1 token address. ParameterDescription_l1TokenThe address of l1 token. updateTokenMapping function updateTokenMapping(address _l1Token, address _l2Token) external; Update the mapping that connects an ERC20 token from L1 to L2. ParameterDescription_l1TokenThe address of the ERC20 token in L1._l2TokenThe address of corresponding ERC20 token in L2. L2 Gateway API withdrawETH function withdrawETH(address to, uint256 amount, uint256 gasLimit) external payable; Sends ETH from L2 to L1. ParameterDescriptiontoThe address of recipient’s account on L1.amountThe amount of token to transfer, in wei.gasLimitGas limit required to complete the deposit on L1. This is optional, send 0 if you don’t want to set it. withdrawERC20 function withdrawERC20(address token, address to, uint256 amount, uint256 gasLimit) external payable; Sends ERC20 tokens from L2 to L1. ParameterDescriptiontokenThe token address on L2.toThe address of recipient’s account on L1.amountThe amount of token to transfer, in wei.gasLimitGas limit required to complete the deposit on L1. This is optional, send 0 if you don’t want to set it. getL1ERC20Address function getL1ERC20Address(address l2Token) external view returns (address); Returns the corresponding L1 token address given an L2 token address. ParameterDescriptionl2TokenThe address of the L2 token. updateTokenMapping function updateTokenMapping(address _l1Token, address _l2Token) external; Update the mapping that connects an ERC20 contract from L2 to L1. ParameterDescription_l2TokenThe address of the ERC20 token in L2._l1TokenThe address of corresponding ERC20 token in L1. What's Next ERC721 NFT Bridge MoreEdit this pageJoin our community On This PageDeposit ETH and ERC20 tokens from L1Withdraw ETH and ERC20 tokens from L2Creating an ERC20 token with custom logic on L2Adding a Custom L2 ERC20 token to the Scroll BridgeL1 Gateway APIdepositETHdepositERC20getL2ERC20AddressupdateTokenMappingL2 Gateway APIwithdrawETHwithdrawERC20getL1ERC20AddressupdateTokenMapping MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us | Parameter | Description | || --- || --- || | to | The address of recipient’s account on L2. | | amount | The amount of token to transfer, in wei. | | gasLimit | Gas limit required to complete the deposit on L2. 170000 should be enough to process the transaction, but unused funds are refunded. | | Parameter | Description | || --- || --- || | token | The token address on L1. | | to | The address of recipient’s account on L2. | | amount | The amount of token to transfer, in wei. | | gasLimit | Gas limit required to complete the deposit on L2. 200000 should be enough to process the transaction, depending on the Gateway, but unused funds are refunded. | | Parameter | Description | || --- || --- || | _l1Token | The address of l1 token. | | Parameter | Description | || --- || --- || | _l1Token | The address of the ERC20 token in L1. | | _l2Token | The address of corresponding ERC20 token in L2. | | Parameter | Description | || --- || --- || | to | The address of recipient’s account on L1. | | amount | The amount of token to transfer, in wei. | | gasLimit | Gas limit required to complete the deposit on L1. This is optional, send 0 if you don’t want to set it. | | Parameter | Description | || --- || --- || | token | The token address on L2. | | to | The address of recipient’s account on L1. | | amount | The amount of token to transfer, in wei. | | gasLimit | Gas limit required to complete the deposit on L1. This is optional, send 0 if you don’t want to set it. | | Parameter | Description | || --- || --- || | l2Token | The address of the L2 token. | | Parameter | Description | || --- || --- || | _l2Token | The address of the ERC20 token in L2. | | _l1Token | The address of corresponding ERC20 token in L1. | ```plaintext interface IScrollStandardERC20 { /// @notice Return the address of Gateway the token belongs to. function gateway() external view returns (address); /// @notice Return the address of counterpart token. function counterpart() external view returns (address); /// @dev ERC677 Standard, see https://github.com/ethereum/EIPs/issues/677 /// Defi can use this method to transfer L1/L2 token to L2/L1, /// and deposit to L2/L1 contract in one transaction function transferAndCall(address receiver, uint256 amount, bytes calldata data) external returns (bool success); /// @notice Mint some token to recipient's account. /// @dev Gateway Utilities, only gateway contract can call /// @param _to The address of recipient. /// @param _amount The amount of token to mint. function mint(address _to, uint256 _amount) external; /// @notice Burn some token from account. /// @dev Gateway Utilities, only gateway contract can call /// @param _from The address of account to burn token. /// @param _amount The amount of token to mint. function burn(address _from, uint256 _amount) external;} ``` ```plaintext function depositETH(address _to, uint256 _amount, uint256 _gasLimit) public payable; ``` ```plaintext function depositERC20(address _token, address _to, uint256 _amount, uint256 _gasLimit) payable; ``` ```plaintext function getL2ERC20Address(address _l1Token) external view returns (address); ``` ```plaintext function updateTokenMapping(address _l1Token, address _l2Token) external; ``` ```plaintext function withdrawETH(address to, uint256 amount, uint256 gasLimit) external payable; ``` ```plaintext function withdrawERC20(address token, address to, uint256 amount, uint256 gasLimit) external payable; ``` ```plaintext function getL1ERC20Address(address l2Token) external view returns (address); ``` ```plaintext function updateTokenMapping(address _l1Token, address _l2Token) external; ``` # L1 and L2 Bridging | Scroll Documentation ## https://docs.scroll.io/en/developers/l1-and-l2-bridging/ Skip to Content Getting Started Developers Technology Learn SDK English English Español 中文 Türkçe Navigation Getting Started Developers Technology Learn SDK Developers Building on Scroll Developer Quickstart Verifying Smart Contracts Scroll Contracts Ethereum & Scroll Differences L1 & L2 Bridging ETH & ERC20 Token Bridge ERC721 NFT Bridge ERC1155 Token Bridge The Scroll Messenger Transaction Fees on Scroll Tooling deployed On Scroll Guides Contract Deployment Tutorial Scroll Messenger Cross-chain Interaction Bridge an ERC20 through the Custom Gateway Running a Scroll Node Checking Transaction Journey What to Build Stablecoin Payments Tutorial Solidity Cookbook Privacy dApps with ZK Mainnet Resources Rollup Explorer Scrollscan Explorer Sepolia Resources Rollup Explorer Scroll Sepolia Explorer L1 and L2 Bridging On This PageL1 Gateway architectureL2 Gateway architecture The Scroll bridge enables the transfer of ETH, ERC20 tokens, NFTs, and arbitrary messages between L1 and L2. It serves as a secure mechanism for moving various digital assets across L1 and L2. To facilitate the transfer of ETH and ERC20 tokens, the Scroll bridge utilizes the Gateway Router. This contract ensures the smooth passage of these assets between L1 and L2, allowing users to transfer their Ethereum-based tokens seamlessly. The ERC721 and ERC1155 Gateway enables the transfer of non-fungible assets between the two networks, allowing users to move their NFTs across L1 and L2. In addition to token transfers, the Scroll Messenger contract enables cross-chain contract interaction. This means that contracts on one network can interact with contracts on the other network through the Scroll Messenger contract. This functionality expands the possibilities for decentralized applications and smart contracts to operate seamlessly across both networks. L1 Gateway architecture There are many entry points from the user to the Scroll bridge. This will depend on what you want to do and how you want to do it. If you want to send ETH or ERC20 tokens, you should use the GatewayRouter. If you want to send NFTs, you should use the L1ERC721Gateway or L1ERC1155Gateway. If you want to send arbitrary data, you should use the L1ScrollMessenger. All Gateway transfers use the Scroll Messenger to send assets cross-chain, whose job is to append the transactions to the Message Queue for L2 inclusion. L2 Gateway architecture Regarding possible permissionlessly callable entry points, the L2 Gateway Architecture is very similar to L1. The difference is that when sending a message from L2, calling the appendMessage function will store the message in an append-only binary merkle tree (aka withdraw tree) in the L2MessageQueue. When a new message is sent to the L2MessageQueue, the relayer will detect it and store it in the database. When the block is finalized, it will generate a proof of the new merkle path and pass it to the L1geth node to execute on L1ScrollMessenger. All finalized withdraw roots will be stored in the rollup contract so we can verify the proof against them. In the next Scroll versions, the Relayer won’t be needed since all users will be able to finalize the transaction on L1. In the upcoming sections, we will explore the technical aspects of the bridge, including the smart contract API required to utilize its capabilities. Detailed guides with code examples are provided in the Developer Guides section to assist developers and users in understanding and implementing these functionalities. What's Next ETH & ERC20 Token Bridge MoreEdit this pageJoin our community On This PageL1 Gateway architectureL2 Gateway architecture MoreEdit this pageJoin our community Feedback Stay up-to-date on the latest Scroll Developer newsRoadmap updates, virtual and live events, ecosystem opportunities and moreThank you for subscribing! About ScrollBug BountyJoin UsHealth StatusPrivacy PolicyTerms and ConditionsResourcesBlogDocumentationBrand KitAuditsFollow Us