π The Ethereum blockchain has great decentralization & security properties. These properties come at a price: transaction throughput is low, and transactions can be expensive (search term: blockchain trilemma). This makes many traditional web applications infeasible on a blockchain... or does it?
π° A number of approaches to scaling have been developed, collectively referred to as layer-2s (L2s). Among them is the concept of payment channels, state channels, and state channel networks. This tutorial walks through the creation of a simple state channel application, where users seeking a service lock collateral on-chain with a single transaction, interact with their service provider entirely off-chain, and finalize the interaction with a second on-chain transaction.
π§βπ€βπ§ State channels really excel as a scaling solution in cases where a fixed set of participants want to exchange value-for-service at high frequency. The canonical example is in file sharing or media streaming: the server exchanges chunks of a file in exchange for micropayments.
π§ In our case, the service provider is a Guru who provides off-the-cuff wisdom to each client Rube through a one-way chat box. Each character of text that is delivered is expected to be compensated with a payment of 0.01 ETH.
π Read more about state channels in the Ethereum Docs.
β OpenZepplin's ECDSA Library provides an easy way to verify signed messages, but for this challenge we'll write the code ourselves.
- π£οΈ Build a
packages/hardhat/contracts/Streamer.solcontract that collects ETH from numerous client addresses using a payablefundChannel()function and keeps track ofbalances. - π΅ Exchange paid services off-chain between the
packages/hardhat/contracts/Streamer.solcontract owner (the Guru) and rube clients with funded channels. The Guru provides the service in exchange for signed vouchers which can later be redeemed on-chain. - β± Create a Challenge mechanism with a timeout, so that rubes are protected from a Guru who goes offline while funds are locked on-chain (either by accident, or as a theft attempt).
- β Consider some security / usability holes in the current design.
π¬ Meet other builders working on this challenge and get help in the State Channel Telegram!
Before you begin, you need to install the following tools:
- Node (v18 LTS)
- Yarn (v1 or v2+)
- Git
Then download the challenge to your computer and install dependencies by running:
git clone https://github.com/scaffold-eth/se-2-challenges.git challenge-5-state-channels
cd challenge-5-state-channels
git checkout challenge-5-state-channels
yarn installin the same terminal, start your local network (a blockchain emulator in your computer):
yarn chainin a second terminal window, π° deploy your contract (locally):
cd challenge-5-state-channels
yarn deployin a third terminal window, start your π± frontend:
cd challenge-5-state-channels
yarn startπ± Open http://localhost:3000 to see the app.
π©βπ» Rerun
yarn deploywhenever you want to deploy new contracts to the frontend. If you haven't made any contract changes, you can runyarn deploy --resetfor a completely fresh deploy.
Like the token vendor challenge, we'll be building an Ownable contract. The contract owner is the Guru (the service provider in this application), and you will use multiple browser windows or tabs to assume the roles of Guru and rube (service provider & client).
π
contract StreamerinheritsOwnablewith theiskeyword.Ownablecomes from openzeppelin-contracts - a collection of high quality smart contract library code.
π In
packages/hardhat/deploy/00_deploy_streamer.js, uncomment the lines of code that deploy the contract and transfer ownership. You will need to enter your own frontend address, to act as Guru.
You'll have to redeploy with yarn deploy --reset.
We'll need another active address to act as the rube in our app. To do this just open a new tab in your browser.
β οΈ Note: previous challenges created new addresses by opening an incognito window or a different browser. This will not work for this challenge, because the off-chain application uses a very simple communication pipe that doesn't work between different browsers or private windows.
- Does your original frontend address receive the
Hello GuruUI? - Does your alternate addresses receive the
Hello RubeUI?
Like the decentralized staking challenge, we'll track balances for individual channels / users in a mapping:
mapping (address => uint256) balances;
Rubes seeking wisdom will use a payable fundChannel() function, which will update this mapping with the supplied balance.
π Edit
packages/hardhat/contracts/Streamer.solto complete thefundChannel()function
π Check
packages/nextjs/app/streamer/page.tsxto see the frontend calling this function. (ctrl-f fundChannel)
Run
yarn deployand open a channel in the Rube's tab. (You may need some funds from the faucet)
- Does opening a channel cause a
Received Wisdombox to appear? - Do opened channels appear on the Guru's UI as well?
- Using the Debug Contracts tab, does a repeated call to
fundChannelfail?
Now that the channel is funded and all participants have observed the funding via the emitted event, we can begin our off-chain exchange of service. We are now working in packages/nextjs/app/streamer/page.tsx.
Functions of note:
provideService: The Guru sends wisdom over the wire to the client.reimburseService: The rube creates a voucher for the received service, signs it, and returns it.processVoucher: The service provider receives and stores vouchers.
The first two functions are complete - we will work on processVoucher, where the service provider examines returned payments, confirms their authenticity, and stores them.
π Edit
packages/nextjs/app/streamer/page.tsxto complete theprocessVoucher()function and secure this off-chain exchange. You'll need to recreate the encoded message that the client has signed, and then verify that the received signature was in fact produced by the client on that same data.
- Secure your service! Validate the incoming voucher & signature according to instructions inside
processVoucher() - With an open channel, start sending advice. Can you see the claimable balance update as service is rendered? This should happen only if rube has "Autopay" active.
- Can
provideServicebe modified to prevent continued service to clients who don't keep up with their payments?
π¬ Hint: You'll want to compare the size of your best voucher against the size of your provided wisdom. If there's too big a discrepency, cut them off!
Now that we've collected some vouchers, we'd like to redeem them on-chain and move funds from the Streamer contract's balances map to the Guru's own address. The withdrawEarnings function of packages/hardhat/contracts/Streamer.sol takes a Struct named voucher (balance + signature) as input, and should:
- Recover the signer using
ecrecover(bytes32, uint8, bytes32, bytes32)on theprefixedHashedmessage and supplied signature.- Hint:
ecrecovertakes the signature in its decomposed form withv,,r, andsvalues. The string signature produced inpackages/nextjs/app/streamer/page.tsxis just a concatenation of these values, which we split usingethers.Signature.fromto create the on-chain friendly signature. Read about the ecrecover function here
- Hint:
- Check that the signer has a running channel with balance greater than the voucher's
updatedBalance - Calculate the payout (
balances[signer] - updatedBalance) - Update the channel balance.
- Send the payout to the Guru.
π‘ Reminders:
- Changes to contracts must be redeployed to the local chain with
yarn deploy --reset. - For troubleshooting / debugging, your contract can use hardhat's
console.log, which will print to your console running the chain.
π Edit
packages/hardhat/contracts/Streamer.solto complete thewithdrawEarnings()function as described.
π Edit
packages/nextjs/app/streamer/page.tsxto enable the UI button for withdrawals.
- Recover funds on-chain for services rendered! After the Guru submits a voucher to chain, you should be able to see the wallet's ETH balance increase.
-
withdrawEarningsis a function that only the service provider would be interested in calling. Should it be markedonlyOwner? (theonlyOwnermodifier makes a function accessible only to the contract owner - anyone else who tries to call it will be immediately rejected).
So far so good:
- Rubes can connect to the Guru via an on-chain deposit.
- The pair can then transact off-chain with high throughput.
- The Guru can recover earnings with their received vouchers.
But what if a rube is unimpressed with the service and wishes to close a channel to recover whatever funds remain? What if the Guru is a no-show after the initial channel funding deposit?
A payment channel is a cryptoeconomic protocol - care needs to be taken so that everyone's financial interests are protected. We'll implement a two step challenge and close mechanism that allows rubes to recover unspent funds, while keeping the Guru's earnings safe.
π Edit
packages/hardhat/contracts/Streamer.solto create a publicchallengeChannel()function.
π Edit
packages/nextjs/app/streamer/page.tsxto enable the challenge and closure buttons for service clients(rubes).
The challengeChannel() function should:
- Check in the
balancesmap that a channel is already open in the name ofmsg.sender - Declare this channel to be closing by setting
canCloseAt[msg.sender]toblock.timestamp + 30 seconds - Emit a
Challengedevent with the sender's address.
The emitted event gives notice to the Guru that the channel will soon be emptied, so they should apply whatever vouchers they have before the timeout period ends.
π Edit
packages/hardhat/contracts/Streamer.solto create a publicdefundChannel()function.
The defundChannel() function should:
- Check that
msg.senderhas a channel that can be closed, by ensuring a non-zerocanCloseAt[msg.sender]is before the current timestamp. - Transfer
balances[msg.sender]to the sender. - Emit a
Closedevent.
β Make sure the defundChannel declaration is uncommented in
packages\nextjs\app\streamer\page.tsx
- Launch a challenge as a channel client. If wisdom has been given, the Guru's UI should show an alert via their
Cash out latest voucherbutton. - Recover the Guru's best voucher before the channel closes.
- Close the channel and recover rube funds.
- Currently, the service provider has to manually submit their vouchers after a challenge is registered on chain. Should their channel wallet do that automatically? Can you implement that in this application?
- Suppose some rube enjoyed their first round of advice. Is it safe for them to open a new channel with
packages/hardhat/contracts/Streamer.sol? (Hint: what data does the Guru still hold?)
- Now is a good time to run
yarn testto run the automated testing function. It will test that you hit the core checkpoints. You are looking for all green checkmarks and passing tests!
π‘ Edit the defaultNetwork to your choice of public EVM networks in packages/hardhat/hardhat.config.ts
π You will need to generate a deployer address using yarn generate This creates a mnemonic and saves it locally.
π©βπ Use yarn account to view your deployer account balances.
β½οΈ You will need to send ETH to your deployer address with your wallet, or get it from a public faucet of your chosen network.
π Run yarn deploy to deploy your smart contract to a public network (selected in hardhat.config.ts)
π¬ Hint: You can set the
defaultNetworkinhardhat.config.tstosepoliaOR you canyarn deploy --network sepolia.
βοΈ Edit your frontend config in packages/nextjs/scaffold.config.ts to change the targetNetwork to chains.sepolia or any other public network.
π» View your frontend at http://localhost:3000 and verify you see the correct network.
π‘ When you are ready to ship the frontend app...
π¦ Run yarn vercel to package up your frontend and deploy.
Follow the steps to deploy to Vercel. Once you log in (email, github, etc), the default options should work. It'll give you a public URL.
If you want to redeploy to the same production URL you can run
yarn vercel --prod. If you omit the--prodflag it will deploy it to a preview/test URL.
π¦ Since we have deployed to a public testnet, you will now need to connect using a wallet you own or use a burner wallet. By default π₯
burner walletsare only available onhardhat. You can enable them on every chain by settingonlyLocalBurnerWallet: falsein your frontend config (scaffold.config.tsinpackages/nextjs/)
By default, π Scaffold-ETH 2 provides predefined API keys for popular services such as Alchemy and Etherscan. This allows you to begin developing and testing your applications more easily, avoiding the need to register for these services.
This is great to complete your SpeedRunEthereum.
For production-grade applications, it's recommended to obtain your own API keys (to prevent rate limiting issues). You can configure these at:
-
π·
ALCHEMY_API_KEYvariable inpackages/hardhat/.envandpackages/nextjs/.env.local. You can create API keys from the Alchemy dashboard. -
π
ETHERSCAN_API_KEYvariable inpackages/hardhat/.envwith your generated API key. You can get your key here.
π¬ Hint: It's recommended to store env's for nextjs in Vercel/system env config for live apps and use .env.local for local testing.
Run the yarn verify --network your_network command to verify your contracts on etherscan π°
π Search this address on Etherscan to get the URL you submit to πββοΈSpeedRunEthereum.com.
π Head to your next challenge here.
π¬ Problems, questions, comments on the stack? Post them to the π scaffold-eth developers chat








