A Starter Kit for dApps where users can create and manage multiple contracts
- 🍦 lean vanilla smart contract factory 🏭
- 💪 use-case flexibility 🌍
- 🧐 mini tutorial 🧭
In order to go through the tutorial it helps if you're already familiar with the amazing scaffold-eth buidl tools. If you're not, for following this tutorial it is recommended that you're at least a web developer with some basic Solidity experience.
🤓 The tutorial presents the essential aspects quite in detail.
If you're an absolute noob to web3, check out the Ethereum Speed Run.
Solidity & React are set up to
- create contracts
- browse created contracts
- interact with created contracts
🧪 Quickly experiment with Solidity using a frontend that adapts to your smart contract:
🚀 Start with a basic master-detail UI, customize it for your needs
𝌋 Debug created contracts with a simil master-detail UI
Prerequisites: Node plus Yarn and Git
clone/fork 🏗 scaffold-eth:
git clone -b factory-setup https://github.com/scaffold-eth/scaffold-eth-examples.git
install and start your 👷 Hardhat chain:
cd scaffold-eth-examples
yarn install
yarn chain
in a second terminal window, start your 📱 frontend:
cd scaffold-eth
yarn start
in a third terminal window, 🛰 deploy your contract:
cd scaffold-eth
yarn deploy
🌍 You need an RPC key for production deployments/Apps, create an Alchemy account and replace the value of ALCHEMY_KEY = xxx
in packages/react-app/src/constants.js
🔏 Edit your smart contract YourContract.sol
in packages/hardhat/contracts
📝 Edit your frontend App.jsx
in packages/react-app/src
💼 Edit your deployment scripts in packages/hardhat/deploy
📱 Open http://localhost:3000 to see the app
Whether you're a web3 noob or experienced dev, the following tutorial is a good way to
- get more familiar with scaffold-eth
- learn some ideas for design patterns
If you're in for the tutorial, you're in for a treat! 🍭 🤓 Here's what we'll look at
- Explore the setup - what can a user do?
- Technicalities - how is it built so far?
- UX challenges - where can you take it from here?
🔖 Create and track contracts that each have a "purpose" variable
In the "Your Contracts" tab, create a new contract. The dialog keeps the user informed. This is a common UX pattern beyond the generic tx status notifications.
🗺 Browse all contracts in a list :
<CreatedContractsUI/>
Your new contract should have appeared in the UI. Create a second contract. Observe how the list updates automatically as soon as the transaction is mined.
List items right now only contain data that was available at the moment when the contracts were created.
🕹 Interact with any particular contract in a detail view:
<YourContract/>
Click on a contract to enter the detailed view.
Click any button to change its purpose.
🔐 Access controls are in place
Open a new browser window in incognito mode, go to localhost:3000.
Here you won't be able to change the purpose of existing contracts. In this incognito window you are someone else (notice the address at the top of the window). The current signer is not the owner of those contracts.
𝌋 The Debug UI enables raw interaction with the factory and any created contract instance.
🧐 Check out the "Debug Contracts" tab.
- See what the public functions of YourContractFactory allow you to do. Do you find them useful?
- What else might be useful to have in there?
** 👩💻 😍 UX 😍 🧑💻 Frontend Side Quest - Improve UX when setting the purpose **
Return to the UI where you have 2 buttons to set the purpose of a contract.
Issue: if you click any of the buttons, both show a spinner while the TX is pending.
Your challenge: find a way to obtain this instead
-
The core functionality of your app
-
Right now it only has a purpose that can be changed by the owner.
-
Creates instances of YourContract and keeps track of them all.
-
Kept as lean as possible.
The setup allows users to create their own YourContracts and control them independent from the factory contract.
As a starting point for developing dApps with this setup, we want loose coupling:
- keep created contracts unaware of the factory
- keep the factory unaware of what created contracts actually do
All our factory needs to know is the addresses of created contracts
We emit events on contract creation, so the frontend can easily retrieve a list of all.
We've included useful data in those events.
👩💻 😍 UX 😍 🧑💻 In a dApp based on a setup like ours, user-given individual contract names are probably a good feature to have.
We've adopted a simple and cheap solution: the user-given name is put in the creation event. If the name doesn't need to change over time this approach works fine.
This retrieval happens via a single RPC call by using the useEventListener
hook.
The retrieval happens once on every new block.
It's good to something like this in mind in order to have your app scale well when the UI is rich and lots of users are using it at the same time. It may produce many RPC calls.
For contract state, like "purpose", contract owner, etc. the frontend uses the address of a particular YourContract intance address to read from the contract, which under the hood makes separate RPC calls.
This is what we do in <YourContract/>
You may skip this section and tackle Challenge 1 below, if you're eager to code some more. Just make sure to return here some time later.
Understanding this is crucial if you're serious about building factory pattern dApps, so you'll need to do it anyway. But no pressure right now 😎 🧉
📝 If you're familiar with scaffold-eth, notice the pattern: we dynamically inject the
YourContract
abi and the particular contract instance address into a locally createdcontractConfig
.After that it's business as usual with
useContractLoader
.Without the injection there would be no abi at all for
YourContract
in the config.Why?
Because we never deployed
YourContract
in our hardhat setup.🧐 Looking more closely you'll notice the file
react-app/contracts/hardhat_non_deployed_contracts.json
This one is usually not present because we usually include all our contracts when we
yarn deploy
. Each one gets a fixed deployment address there.But in our factory setup, the
YourContract
instances are created on-chain. Only then they get their addresses, which are stored both in the factory contract state and in the contract creation events.So
yarn deploy
, instead of deploying any particular YourContract, just makes the abi of YourContract available to the frontend for later use. It puts it into the json file above. Later it can be injected at runtime in combination with a specific address.
Lets show our users when and how purpose changes happen!
Find the Solidity code related to SetPurpose events. Uncomment it.
Redeploy with yarn deploy --reset
Find the React code that displays SetPurpose events in <YourContract/>
. It is commented out, uncomment it.
Create a new contract. Change its purpose.
Now, for any particular instance of YourContract, our app
- displays contract events
- displays contract state
- enables contract interaction
Our factory ensures that the user who creates a contract also becomes the owner
Without this code, the factory would remain the owner of all YourContract instances.
Suppose we wanted to display the owner of any contract in the master view. Probably your users want to easily identify the contracts they've created.
The owner can change over time, unlike the creator. We can't build this feature by using contract creation event data.
🤔 How do we get the owners of all contracts?
In each <ContractItem />
, we apply the pattern from <YourContract/>
: 💉 we dynamically inject the abi & address, so we can read from each particular contract instance.
Go to ContractItem.jsx
and find the code that fetches owner data. Uncomment it. Find the code that displays this data. Uncomment that.
Now you should see owner information in the contracts list of the master view.
Owner addresses are quite hard to read. In the contracts list, let's mark items which belong to the current user so they may be identified more easily.
Go to the code inside the <ContractItem/>
component. Find the commented code which marks the item when the contract owner is the current user. Uncomment it.
You should now see contract items like this:
☑️ Test the functionality by creating contracts from an incognito window. Compare the views of different users.
What if there were 100 contracts?
As soon as you receive the creation event data in
App.jsx
, would you make a total of 100 requests for reading the owner of each contract within its<ContractItem />
component?
It's probably better if we retrieve the owner of a particular contract item only when the item is actually in view.
Here is a simple solution for that:
- This would improve the UX a lot, whether we display contract owners or not
- If you allow n contracts per page, only n calls to read the owner will be made at once.
👩💻 😍 🧑💻 Allow users to filter contracts by name in the list view
- use an input field
- how do you combine this with the pagination feature?
👩💻 😍 🧑💻 Allow users to filter contracts by only listing their own ones
- use a switch or checkbox "only mine"
- how do you combine this with the pagination feature?
A factory setup can quickly get very complex, especially if you want to provide good UX.
Your real-world project will probably need code design improvements in order to be able to scale well and be easy to use.
- good routing
- efficient data retrieval (RPC nodes)
- different empty states (waiting for data, data not available, no account connected)
- clean code
Some design patterns to help you grow can be found in this repo.
- master-detail UI pattern with shareable links to detail pages (routing with react router v6)
- a pattern on how to create a contract specific react context when opening a contract in the UI
- strategies to minimize number of RPC calls while using eth-hooks v2.
eth-hooks v4 is a much more advanced toolkit but it requires you to use the typescript flavored scaffold-eth.
Our approaches in solving UX challenges depend on many factors. If your project is going to have lots of complex data to retrieve, you'll probably also use a subgraph or other blockchain indexing tools. These are more capable than the useEventListener
hook we've used here. This would impact how you approach scaling your dApp.
There are many use cases for a setup similar to ours here. Take Uniswap:
- users create liquidity pools
- each liquidity pool is a separate contract
Sometimes the created contracts may be more tightly coupled to the factory - it depends on the use case: how much control over the contracts should a user have / should the factory keep?
** 🧙♂️ 🧝♀️ 🧞♂️ Advanced Contract Design Quest: Dig into the UniswapV3 Docs. Here the factory is indeed more tightly coupled to the created pools.
- Why, do you think, is that?
- How does Uniswap handle fees?
- pool owner fees?
- uniswap fees?
Documentation, tutorials, challenges, and many more resources, visit: docs.scaffoldeth.io
📕 Read the docs: https://docs.soliditylang.org
📚 Go through each topic from solidity by example editing YourContract.sol
in 🏗 scaffold-eth
📧 Learn the Solidity globals and units
Check out all the active branches, open issues, and join/fund the 🏰 BuidlGuidl!
Join the telegram support chat 💬 to ask questions and find others building with 🏗 scaffold-eth!
🙏 Please check out our Gitcoin grant too!