Create forms in React easily using React Hook Form and Zod

Create forms in React easily using React Hook Form and Zod

Atharva Deosthale

Form handling in React without using any library is a tedious task. What we have been taught before is mapping states to input fields and using the state setter in the onChange function of the input field. This method works, but is tedious to manage in a production-level application (for example, the applications with forms with many input fields). Creating and mapping state to every input field for form management in React doesn't make sense. Thankfully, many amazing developers are out in the community, constantly contributing to the React ecosystem, and have created amazing form-handling libraries to use in your React project. One of them is react-hook-form. This package helps us to create and maintain forms in a very simple and scalable manner. In this article, we will first look at the traditional way of form handling, where we map state to input fields and perform form validation manually. Then, we will use react-hook-form to create the same form and check for ourselves which method is simpler and scalable for production-level applications. 💡 If you prefer to learn about this topic through video, I have created a YouTube video on this topic. Creating a React Application You may skip this part if you already have some sort of React application set up. We will use Next.js for this tutorial as we don’t need to worry about routing in this case. To create a Next.js application, use the following command: npx create-next-app <app-name> Feel free to choose your own app name here. In this tutorial, we have used the app router, the latest and currently standard router Next.js works with. We also choose to install Tailwind CSS for basic styling and use all the default configurations the Next.js installation wizard suggests. Also, make sure that you use TypeScript, as we will be dealing with zod and it works best with TypeScript. Now, before we get started, let’s clean the default styling. Go to globals.css, and only have the following contents in the file: @tailwind base; @tailwind components; @tailwind utilities; 💡 You can find the complete code in the GitHub repository associated with this article. Installing packages Now, let’s install some packages that will help us throughout this article: npm install react-hook-form zod @hookform/resolvers Using the above command, we are installing the following commands: react-hook-form: We will use this package to create forms easily in our application. zod: We will use this package to define the validation rules for our form. @hookform/resolvers: We will use this package to connect our zod-defined validation rules to the form we create using react-hook-form. Now that we have installed all the necessary packages, we can proceed to creating our form in the traditional way and then using react-hook-form. Using states to create a form Now, let’s use the traditional way to create a form. We are going to create a user registration form asking for first name, last name, email, password and password confirmation. Have the following contents under page.tsx file under the app directory. "use client"; import { useState } from "react"; export default function Traditional() { const [email, setEmail] = useState<string>(""); const [password, setPassword] = useState<string>(""); const [confirmPassword, setConfirmPassword] = useState<string>(""); const [firstName, setFirstName] = useState<string>(""); const [lastName, setLastName] = useState<string>(""); function onSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault(); if (!email || !password || !confirmPassword || !firstName || !lastName) { return alert("Please fill out all fields"); } if (password !== confirmPassword) { return alert("Passwords do not match"); } alert("User created successfully!"); } return ( <div className="p-10"> <form className="space-y-5" onSubmit={onSubmit}> <div> <input className="border border-black px-5 py-3" placeholder="Email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} /> </div> <div> <input className="border border-black px-5 py-3" placeholder="Password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} /> </div> <div> <input className="border border-black px-5 py-3" placeholder="Confirm Password" type="password" value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} /> </div> <div> <input className="border border-black px-5 py-3" placeholder="First Name" type="text" value={firstName} onChange={(e) => setFirstName(e.target.value)} /> </div> <div> <input className="border border-black px-5 py-3" placeholder="Last Name" type="text" value={lastName} onChange={(e) => setLastName(e.target.value)} /> </div> <button type="submit" className="bg-black text-white px-5 py-3"> Submit </button> </form> </div> ); } In the above code, we are doing the following: We are telling Next.js that this is a client component and not a server component by mentioning ”use client”; on the top of the file. We are creating five states (email, password, confirmPassword, firstName, lastName) to keep track of respective input fields. We are creating a form using the <form> tag, with the form action being the onSubmit() function. For each input field, we are mapping the respective state. We are setting the value as the actual state value, and using the onChange attribute to use the respective state setters to update the state with the current state of the input field whenever it’s interacted with. In the onSubmit() function, we are preventing the usual form submit behaviour and, in turn, disabling page reload by using e.preventDefault(). We are then performing validation to make sure all fields are filled out. We are also performing validation to check if password and confirmPassword matches. Now, there are a few takeaways from this method: For each input field, there needs to be a state inside the respective component. For smaller forms it’s quite easy to manage. But for larger forms, it will be chaotic with many states. We are performing validation inside the onSubmit() function. In case of any error, we are displaying an alert. For any large form, red text must be near the input field of concern for better UX, not an alert box. To achieve that, using this method, each input field must have another state for any errors, and then these errors must be set using the state in the onSubmit() function. I don’t know about you, but this sure sounds super tedious to me. Now, let’s see how react-hook-form fixes these issues and provides a better form experience overall. Using react-hook-form to create a form Creating a form using this method requires two steps: creating a zod schema, defining the form validation rules, and creating the form based on the zod schema. Creating zod validation schema In the src directory, create a new directory called types and create a file called input.ts within this directory. We will create our zod schema in this file. Have the following contents in the file: import { z } from "zod"; const registerInputSchema = z .object({ email: z.string().min(1, "Email is required").email(), password: z .string() .min(5, "Password should be minimum 5 characters") .max(15, "Password should not exceed 15 characters"), confirmPassword: z .string() .min(5, "Password should be minimum 5 characters") .max(15, "Password should not exceed 15 characters"), firstName: z.string().min(1, "First name is required"), lastName: z.string().min(1, "Last name is required"), }) .refine((data) => data.password === data.confirmPassword, { message: "Passwords do not match", path: ["confirmPassword"], }); type RegisterInput = z.infer<typeof registerInputSchema>; export { registerInputSchema }; export type { RegisterInput }; In the above code, we are doing the following: We are creating a zod schema for all of the input fields we are going to create in the form. We are specifying the specific validation rules for each field. For example, for email, the rules are: it must be a string it is a required field (minimum characters 1) it must be an email (follow the email format) For password and confirmPassword fields, we have a rule that the passwords must be between 5 and 15 characters in length. For firstName and lastName we have a minimum character requirement of 1 to make the field required. We are using refine on the schema to add validation further to check if password and confirmPassword match. If not, an error will be shown on the confirmPassword field. We are creating a type based on the schema for type-safety during the creation of the form. Finally, we are exporting the schema and the type so that we can use those in our component. Now, let's create a separate page in our application for the react-hook-form method. Creating the form Create a new folder under the app directory called rhf. Within this new folder, create a new file called page.tsx. Have the following contents in the file: "use client"; import { useForm } from "react-hook-form"; import { registerInputSchema } from "@/types/inputs"; import type { RegisterInput } from "@/types/inputs"; import { zodResolver } from "@hookform/resolvers/zod"; export default function RFH() { const { register, formState: { errors }, handleSubmit, } = useForm<RegisterInput>({ resolver: zodResolver(registerInputSchema), }); function onSubmit(data: RegisterInput) { console.log(data); alert("User created successfully!"); } return ( <div className="p-10"> <form className="space-y-5" onSubmit={handleSubmit(onSubmit)}> <div> {errors.email && ( <p className="text-red-600">{errors.email.message}</p> )} <input className="border border-black px-5 py-3" placeholder="Email" type="email" {...register("email")} /> </div> <div> {errors.password && ( <p className="text-red-600">{errors.password.message}</p> )} <input className="border border-black px-5 py-3" placeholder="Password" type="password" {...register("password")} /> </div> <div> {errors.confirmPassword && ( <p className="text-red-600">{errors.confirmPassword.message}</p> )} <input className="border border-black px-5 py-3" placeholder="Confirm Password" type="password" {...register("confirmPassword")} /> </div> <div> {errors.firstName && ( <p className="text-red-600">{errors.firstName.message}</p> )} <input className="border border-black px-5 py-3" placeholder="First Name" type="text" {...register("firstName")} /> </div> <div> {errors.lastName && ( <p className="text-red-600">{errors.lastName.message}</p> )} <input className="border border-black px-5 py-3" placeholder="Last Name" type="text" {...register("lastName")} /> </div> <button type="submit" className="bg-black text-white px-5 py-3"> Submit </button> </form> </div> ); } In the above code, we have created the form using react-hook-form. Let's see what we have done here step-by-step. We are using the useForm hook imported from react-hook-form and restructuring register, formState: { errors } and handleSubmit. register would be used for every input field we create so that react-hook-form can configure and track the input field. errors will contain any validation errors caused in any specific input field. handleSubmit will be used to call a function after all validation is passed. We are providing the exported type RegisterInput to the useForm hook to obtain type safety throughout the creation of the form. We are using the zodResolver from @hookforms/resolvers/zod to connect our zod schema to the form instance so that all the validations for the input fields are performed automatically. For the form onSubmit we are using handleSubmit(onSubmit) so that the onSubmit() function is only executed after all validation checks have passed after the form is submitted. For each input field in the form, we are adding {...register("<name>")} where <name> corresponds to the field name in the zod schema. This way react-hook-form can add necessary props to the input field. Once you submit the form with errors, you won't see an alert box. Instead, you will see errors right above the input field in red colour. You'll also notice that we aren't using states at all in the component. If you want to add another input field or change the validation rules, you can do that directly in the zod schema we created earlier and change the input fields accordingly. Conclusion React hook form makes it much easier for you to create forms in a production-level React application. Should you use it for smaller applications with forms only having one or two input fields? Probably not, but you certainly can. One thing's for sure- for large forms or a production-level application, this package will help a ton. I hope you received value from this article! Please share it with your friends and subscribe to the newsletter if you'd love to see more articles from me in your inbox.

Writing a factory for MultiSig contract using Solidity

Writing a factory for MultiSig contract using Solidity

Atharva Deosthale

In the previous article in this series, we saw how you could create your multi-sig wallet contract using Solidity. In this article, we will continue the journey and create a contract factory for that multi-sig contract we developed earlier. This will allow anyone to deploy their multi-sig contract through your contract (and the app we will make later in the series) and enable cheap deployments. This contract factory will clone the already-deployed multi-sig contract and configure it according to the user deploying it. We will also have a built-in registry to track who deployed the contract and the list of contracts deployed by a specific address. If you prefer video tutorials, I have a video on this same topic on my YouTube channel. Make sure you go watch the video if you prefer videos over articles! Changes to the multi-sig contract To make our multi-sig contract proxy-friendly and deployable by our contract factory, we need to make a minor change to our multi-sig contract. Instead of using a constructor, we need to use a regular function, which we will name initialize. This is because when a contract is cloned, the constructor is not called, so there's no way for us to set the owners just after the contract is deployed. Luckily, OpenZeppelin has a Initializable contract that helps us make our custom function act like a constructor, i.e., make it so that it can only be run once, just like a constructor. So, when the contract is deployed, the initialize function is called to do the job constructor was supposed to do and after that, initialize function cannot be used on the same contract again! Add the following import to your Multisig.sol contract: import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; Now, you need to inherit this contract: contract Multisig is Initializable { ... } Now, let's edit the constructor to be a function named initialize: function initialize(address[] memory owners, uint256 requiredSignatures) public initializer { require(owners.length > 0, "At least one owner required"); require(requiredSignatures > 0 && requiredSignatures <= owners.length, "Invalid number of required signatures"); _owners = owners; _requiredSignatures = requiredSignatures; } In the above code, we are also using the initializer modifier that comes with Initializable contract that makes this function to be able to run only once. Creating the factory contract Now that we made our multi-sig contract factory deployment friendly, we can proceed to write our factory contract. Create a new file under contracts folder named MultisigFactory.sol. Importing OpenZeppelin libraries We need to use some OpenZeppelin libraries here as well: import "@openzeppelin/contracts/proxy/Clones.sol"; import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; Clones allow us to clone any contract just by providing the contract address. If you look at the source code, it's all assembly. Under the hood, it's creating a contract that points to an existing implementation. EnumerableSet will allow us to have an AddressSet and have easy functions to deal with addresses. We will use this to save all the contracts deployed by a particular wallet (basically acting like a registry). States and events Our contract factory will have the following states and events: using EnumerableSet for EnumerableSet.AddressSet; address public owner; address public implementation; mapping(address => EnumerableSet.AddressSet) private deployments; event ImplementationUpdated(address _caller, address _implementation); event ContractDeployed(address _deployer, address _deployedContract, address _implementation); owner is the owner of the contract factory that is allowed to update the implementation address. implementation is the contract that will be cloned when someone calls the deployContract() function, which we will take a look at later. deployments keep track of the multi-sig contracts deployed by wallets. You can think AddressSet to be an array of addresses but with easy functions available. ImplementationUpdated event is fired when the contract owner updates the implementation, changing the contract deployed moving forward. ContractDeployed event is fired whenever someone deploys a contract. It will be super useful when we move to the app part to detect when the contract is successfully deployed. Writing the constructor Since a factory is not deploying this contract, and we are deploying it the normal way, we can use the constructor here: constructor(address _implementation) { owner = msg.sender; implementation = _implementation; } In the above code, we set the contract owner and the implementation. Writing the setImplementation() function This function can only be called by the contract owner and will allow the owner to update the implementation that will be cloned moving forward: function setImplementation(address _implementation) public { require(msg.sender == owner, "Not owner!"); implementation = _implementation; emit ImplementationUpdated(msg.sender, _implementation); } In the above code, we check if the function caller is the owner; if not, we revert. We set the implementation and emit the ImplementationUpdated event. Writing the deployContract() function Following is the code for the deployContract() function: function deployContract(bytes memory _data) public { address deployedContract = Clones.clone(implementation); (bool success, ) = deployedContract.call(_data); require(success, "Failed to initialize contract!"); bool added = deployments[msg.sender].add(deployedContract); require(added, "Failed to add to registry!"); emit ContractDeployed(msg.sender, deployedContract, implementation); } The deployContract function is used to create a new Multisig contract instance. It takes a parameter of type bytes, the initialization data for the Multisig contract. The function uses the Clones.clone() function to create a new instance of the Multisig contract using the implementation address stored in the state variable. The initialization data is then passed to the new contract instance using the call() function. If the contract is successfully initialized, the address of the new contract is added to a registry maintained by the contract. Writing the getDeployed() and countDeployed() functions function getDeployed(address _deployer) public view returns(address[] memory) { return deployments[_deployer].values(); } function countDeployed(address _deployer) public view returns(uint256) { return deployments[_deployer].length(); } The above two functions simply access the registry and return relevant data by using functions from AddressSet. Complete contract If you missed something, the following is the complete code for the MultisigFactory.sol contract: // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; import "@openzeppelin/contracts/proxy/Clones.sol"; import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; contract MultisigFactory { using EnumerableSet for EnumerableSet.AddressSet; address public owner; address public implementation; mapping(address => EnumerableSet.AddressSet) private deployments; event ImplementationUpdated(address _caller, address _implementation); event ContractDeployed(address _deployer, address _deployedContract, address _implementation); constructor(address _implementation) { owner = msg.sender; implementation = _implementation; } function setImplementation(address _implementation) public { require(msg.sender == owner, "Not owner!"); implementation = _implementation; emit ImplementationUpdated(msg.sender, _implementation); } function deployContract(bytes memory _data) public { address deployedContract = Clones.clone(implementation); (bool success, ) = deployedContract.call(_data); require(success, "Failed to initialize contract!"); bool added = deployments[msg.sender].add(deployedContract); require(added, "Failed to add to registry!"); emit ContractDeployed(msg.sender, deployedContract, implementation); } function getDeployed(address _deployer) public view returns(address[] memory) { return deployments[_deployer].values(); } function countDeployed(address _deployer) public view returns(uint256) { return deployments[_deployer].length(); } } Writing script to encode data for initialize() function In the factory contract, we have accepted bytes as a parameter in the deployContract() function. Although, we need to encode the call to the initialize() function to pass the bytes to the function. For testing, I've created a script to encode this data. However, our app will do this automatically, and the user won't need to be in the hassle the encode the data. Create a new file under scripts folder named generateEncodedData.js and have the following contents: const hre = require("hardhat"); async function main() { const Lock = await hre.ethers.getContractFactory("Multisig"); const encodedData = await Lock.interface.encodeFunctionData("initialize", [ ["0x53b8E7c9D1e0E9BdF5e2c3197b070542611995e7"], 1, ]); console.log(`Encoded data - ${encodedData}`); } main().catch((error) => { console.error(error); process.exitCode = 1; }); Feel free to replace the addresses with the owner addresses you desire. The script uses the Hardhat framework to retrieve the contract factory for the Multisig contract using the getContractFactory() method. It then calls the encodeFunctionData() method on the contract interface to encode the arguments for the initialize function. The encodeFunctionData() method takes two parameters. The first parameter is a string representing the name of the function to encode, which is "initialize" in this case. The second parameter is an array of values representing the function's arguments. In this script, the arguments for the initialize function are an array containing a single address and an integer value of 1. The script encodes these arguments using the contract interface and outputs the resulting encoded data using the console.log() method. The script output is a string representing the encoded data for the initialize function that can be used to initialize a new instance of the Multisig contract with the specified arguments. Now you can play around, deploy the contract, pass the encoded data to the deployContract function, and you will find the initialize function already called for you and everything set up. If you want to see the complete testing for this contract, watch my YouTube video on the same topic where we deploy the factory contract and deploy new multi-sig contracts through it, along with encoding data for initialize function. Conclusion In this article, you learnt about writing a contract factory for the multi-sig contract we developed in the previous article. Feel free to refer to the previous article in this series. If you have any suggestions about content, feel free to leave them in the comments section, I would love to address them!

Running a local Ethereum test chain - solving the testnet problem!

Running a local Ethereum test chain - solving the testnet problem!

Atharva Deosthale

We all have been to the same place- we write a smart contract to deploy on one of the Ethereum testnets, but you don't have enough funds to deploy this contract. We then use all sorts of testnets and find their faucets to get as many funds as possible to test our contracts. After countless efforts and Twitter verifications, you get very little funds you can barely even use to test. In this case, we will talk about problems while developing contracts on EVM, and finally, we will look at how you can spin up your local node to fix all these problems. The problems with developing on EVM The sole purpose of testnets is to help developers test their contracts on a network before they move to the mainnet, where funds really matter. Ideally, getting these funds should be easy- provisioning funds on-demand should be the top priority of these testnets, although it's a complete nightmare. Instead of focusing on developing their contract, developers are wasting their time getting test funds (which is supposed to be the easiest part). Ethereum recommends Goerli as a testnet to use, although funds on Goerli are so rare that a market has formed around it where people now buy Goerli ETH (yeah, imagine buying testnet funds to test your contracts). I love how Solana devnet handles this; no faucets or anything, you use a CLI command, and you have funds in your wallet instantly each time you run the command. I think testnets should learn something from this and develop some solutions. Creating a local Ethereum chain This section will create our own local Ethereum chain for testing. We will see two ways of doing this- creating a chain from scratch and forking an existing chain and using it as a testnet (so that you have access to all the existing states of the chain, including already-deployed contracts). We will use Hardhat to spin up a local node for both methods. Let's set up a hardhat project. Run the following command in your terminal to initialize a hardhat project: npx hardhat Now you should be presented with a wizard to create your hardhat project. Use any configuration you wish to (although I don't recommend using the empty project option because it has caused problems before). Normally, I choose a JavaScript project. Configuring hardhat.config.js file We need to configure the hardhat.config.js file hardhat created for us to have some details about our test chain. Have the following content in your hardhat.config.js file: /** @type import('hardhat/config').HardhatUserConfig */ module.exports = { solidity: { version: "0.8.9", settings: { optimizer: { enabled: true, runs: 200, }, }, }, networks: { hardhat: { chainId: 1337, }, }, }; In the above config, we are adding a new hardhat object under the networks object. Remember that the name must be hardhat as hardhat recognizes this as a configuration object while we run our node. We have set the chainId to be 1337 which is usually used for local testnet networks. Now, you can use the following command to spin up your local node: npx hardhat node --network hardhat This will start your local node, and you should see something like this in your terminal: Now, you can add a network in your wallet with chain ID 1337 and RPC URL as provided in your terminal. You can also use any default private keys to deploy contracts to your local chain or add them to MetaMask for testing! NOTE: Never use any of the private keys provided by hardhat CLI in production environments. These are publicly known private keys and are only meant for testing! Bots are regularly monitoring these wallets on mainnets and any funds sent to these wallets WILL BE LOST! Always separate test and production wallets. Funding your wallet for testing If you read the warning above, you might want to use your test wallets for testing. However, funds are unavailable in your wallet by default on your local node. Now, we will write a script that gives you any number of local ETH in your wallet for testing. Start by running the following command to install a package: npm install @nomicfoundation/hardhat-network-helpers After the installation is complete, create a new file called init.js inside scripts folder and have the following contents: const helpers = require("@nomicfoundation/hardhat-network-helpers"); async function init() { const address = "your-wallet-address"; await helpers.setBalance(address, 1000 * 1e18); } init() .then(() => process.exit(0)) .catch((error) => { console.error(error); process.exit(1); }); Make sure you replace the value of address variable with the wallet address you want to fund. You can also update the amount as you desire. Now, we need to add some more configuration to the hardhat.config.js file. Here's the updated file: /** @type import('hardhat/config').HardhatUserConfig */ module.exports = { solidity: { version: "0.8.9", settings: { optimizer: { enabled: true, runs: 200, }, }, }, defaultNetwork: "running", networks: { hardhat: { chainId: 1337, }, running: { url: "http://localhost:8545", chainId: 1337, }, }, }; In the above configuration, we add another network called as running and point it to the local node running and use the chain ID the same as the hardhat network, i.e. 1337. We do this to interact with our running hardhat node using scripts. We also set the defaultNetwork to running so that we don't need to provide a network name each time we run a script. Run the following command in your terminal to trigger the addition of funds to your wallet: npx hardhat run ./scripts/init.js After the command is run, you should see your updated funds in your wallet. If it doesn't show up in MetaMask, try switching networks back and forth. While trying local networks on MetaMask, sometimes the transactions outright refuse to go through. This is usually because the network was restarted and the nonce was reset on the network side but is the same on MetaMask. To fix this, go to MetaMask settings -> Advanced -> Reset Wallet (this will not wipe your wallets, it will only reset nonce in the wallet and remove transaction history). Forking an existing network If you want access to contracts deployed on some other chain while testing (such as the USDC token contract or any other contract), forking would be an easy way to achieve that. You can fork any network you wish to; all you need is the RPC URL. You can get a FREE RPC URL without signing-up or connecting your wallet at thirdweb chainlist! hardhat: { chainId: 1337, forking: { url: "RPC_URL", enabled: true, }, }, Make sure to replace RPC_URL with the appropriate RPC URL of the network, you wish to fork. Now, you can start the forked node with the following command: npx hardhat node --network hardhat This will have the same output as before, and you could add more test ETH to your wallet using the same script. However, now you will have access to all the existing deployed contracts, and all the wallets on the network will maintain balances. Conclusion In this article, you learnt how to spin up your local node and start developing your contracts! It's better to use a local node, unlike testnets, most of the time. If you have better ideas on tackling the testnet situation, let me know in the comments below! Make sure to leave any feedback (if any) in the comments as well!

Writing a MultiSig contract using Solidity

Writing a MultiSig contract using Solidity

Atharva Deosthale

If you're new to the world of smart contracts, you may not be familiar with the concept of multi-sig. Multisig, short for multi-signature, is a security feature that requires multiple parties to approve a transaction before it can be executed on the blockchain. In this tutorial, I'll walk you through a simple multi-sig contract written in Solidity. This is the first part of a series where we will be creating a multi-sig dApp from scratch. If you prefer video tutorials instead, I have made a video on this same topic on my YouTube channel. Also, make sure you leave any comments on any dApp idea you want me to build as a tutorial! What does this contract do? This Solidity contract is a multi-sig wallet that allows a group of owners to manage a shared account. Transactions submitted to the contract require a certain number of signatures from the owners before they can be executed. This provides an added layer of security since no single owner can perform transactions without the approval of the required number of owners. Setting up the development environment Creating a hardhat project We will use hardhat in this tutorial to compile and manage our contract. Navigate to a safe directory, and run the following command to initialize the hardhat project initialization wizard: npx hardhat This should load up the initializer. Choose your configuration; in this case, I'll be choosing JavaScript (although we won't be writing scripts in this tutorial, although if you wish to do so, this choice might be important for you). Finally, choose to install the dependencies for your hardhat project. Creating the contract file Hardhat creates a file named Lock.sol within the contracts folder with some example code for you. You can delete the file and create a new file named Multisig.sol. Then you can initialize a simple contract. // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; contract Multisig { } Writing the contract Defining the states Our contract will have states that will store important data about the owners, transaction details and the minimum signatures required for a transaction to get executed. uint256 private _requiredSignatures; address[] private _owners; struct Transaction { address to; uint256 value; bytes data; bool executed; mapping(address => bool) signatures; } Transaction[] private _transactions; The following states have been used in the above code: _requiredSignatures: This variable determines the number of signatures required for a transaction to be executed. It is set in the constructor and cannot be changed once set. _owners: This is an array of addresses representing the owners of the contract. These addresses are set in the constructor and cannot be changed once set. _transactions: This is an array of Transaction structs that represent the pending transactions. Each transaction contains information about the destination address, value, data, whether it has been executed, and a mapping of which owners have signed the transaction. The Transaction struct has the following fields: to: The destination address of the transaction. value: The value to be sent in the transaction. data: Any additional data to be sent with the transaction. executed: A boolean flag indicating whether the transaction has been executed. signatures: A mapping of which owners have signed the transaction. This allows us to easily check if the required number of signatures have been obtained for a given transaction. Defining the events Our contract will have events that we can emit and will be useful when we create an app to know the status of our transaction: event TransactionCreated(uint256 transactionId, address to, uint256 value, bytes data); event TransactionSigned(uint256 transactionId, address signer); event TransactionExecuted(uint256 transactionId, address executer); TransactionCreated event is emitted when one of the owners of the contract creates a transaction for them and others to sign. TransactionSigned event is emitted when someone signs an existing transaction on the contract. TransactionExecuted event is emitted when a transaction is successfully executed. Writing the constructor This code will be run when the contract is first deployed. We will use this to set the owners of the contracts that can only be set by the constructors. We will also set the required number of signatures for a transaction to get executed. constructor(address[] memory owners, uint256 requiredSignatures) { require(owners.length > 0, "At least one owner required"); require(requiredSignatures > 0 && requiredSignatures <= owners.length, "Invalid number of required signatures"); _owners = owners; _requiredSignatures = requiredSignatures; } In the above constructor code, we are running some checks like if the owners array is empty or if the number of signatures required is more than the number of owners supplied. In that case, we revert the transaction. If everything goes well, we set the owners of the contract and also set the required number of signatures. Writing the isOwner() function function isOwner(address account) public view returns (bool) { for (uint256 i = 0; i < _owners.length; i++) { if (_owners[i] == account) { return true; } } return false; } This function takes an address as an argument and returns a boolean indicating whether the given address is an owner of the contract. It does this by iterating through the _owners array and check if the given address matches any of the owners' addresses. Writing the countSignatures() function function countSignatures(Transaction storage transaction) private view returns (uint256) { uint256 count = 0; for (uint256 i = 0; i < _owners.length; i++) { if (transaction.signatures[_owners[i]]) { count++; } } return count; } This function takes a Transaction struct as an argument and returns the number of signatures obtained for that transaction. It does this by iterating through the _owners array and checking if the corresponding owner has signed the transaction. Writing the getTransaction() function function getTransaction(uint256 transactionId) public view returns (address, uint256, bytes memory, bool, uint256) { require(transactionId < _transactions.length, "Invalid transaction ID"); Transaction storage transaction = _transactions[transactionId]; return (transaction.to, transaction.value, transaction.data, transaction.executed, countSignatures(transaction)); } This function takes a transaction ID as an argument and returns information about that transaction, including the destination address, value, data, whether it has been executed, and the number of signatures obtained for that transaction. It does this by looking up the transaction in the _transactions array and calling countSignatures to obtain the number of signatures. Writing the getOwners(), getRequiredSignatures() and receive functions function getOwners() public view returns(address[] memory) { return _owners; } function getRequiredSignatures() public view returns(uint256) { return _requiredSignatures; } receive() external payable {} getOwners() and getRequiredSignatures() functions only return the state values. We have an empty receive() function to enable the contract to receive native tokens. Writing the submitTransaction() function function submitTransaction(address to, uint256 value, bytes memory data) public { require(isOwner(msg.sender), "Not an owner!"); require(to != address(0), "Invalid destination address"); require(value >= 0, "Invalid value"); uint256 transactionId = _transactions.length; _transactions.push(); Transaction storage transaction = _transactions[transactionId]; transaction.to = to; transaction.value = value; transaction.data = data; transaction.executed = false; emit TransactionCreated(transactionId, to, value, data); } This function allows an owner to create a new transaction by providing the destination address, the value of ether to be transferred, and an optional data payload. The function first checks whether the caller is an owner or not, if not it reverts the transaction. The function also checks if the provided destination address is not null and the provided value is greater than or equal to zero, otherwise it reverts the transaction. It then creates a new transaction object and adds it to the _transactions array. The new transaction's ID is set to the length of the array before the new transaction is added, which makes it unique. The Transaction object is initialized with the provided destination address, value, data, and executed set to false. Finally, the function emits an event TransactionCreated with the transaction ID, destination address, value, and data as its parameters. This function does not require any signatures to be added since it only creates a new transaction. Once a transaction has been created, it can be signed by the required number of owners to execute the transaction. Writing the signTransaction() function function signTransaction(uint256 transactionId) public { require(transactionId < _transactions.length, "Invalid transaction ID"); Transaction storage transaction = _transactions[transactionId]; require(!transaction.executed, "Transaction already executed"); require(isOwner(msg.sender), "Only owners can sign transactions"); require(!transaction.signatures[msg.sender], "Transaction already signed by this owner"); transaction.signatures[msg.sender] = true; emit TransactionSigned(transactionId, msg.sender); if(countSignatures(transaction) == _requiredSignatures) { executeTransaction(transactionId); } } This function allows an owner of the contract to sign a pending transaction. The owner must specify the transactionId parameter, which represents the index of the transaction in the _transactions array. The function first checks that the transactionId is valid and that the transaction has not already been executed. It then verifies that the caller is an owner and that they have not already signed the transaction. If all these conditions are met, the function adds the signature of the caller to the signatures mapping of the transaction and emits a TransactionSigned event. Finally, the function checks if the transaction now has enough signatures to be executed. If the required number of signatures is met, the function calls executeTransaction to execute the transaction. Writing the executeTransaction() function function executeTransaction(uint256 transactionId) private { require(transactionId < _transactions.length, "Invalid transaction ID"); Transaction storage transaction = _transactions[transactionId]; require(!transaction.executed, "Transaction already executed"); require(countSignatures(transaction) >= _requiredSignatures, "Insufficient valid signatures"); transaction.executed = true; (bool success,) = transaction.to.call{value: transaction.value}(transaction.data); require(success, "Transaction execution failed"); emit TransactionExecuted(transactionId, msg.sender); } The function first checks whether the provided transaction ID is valid (i.e., it is within the bounds of the _transactions array). It then checks whether the transaction with the provided ID has already been executed. If it has, the function reverts with an error message. The function then checks whether the number of valid signatures on the transaction is greater than or equal to the required number of signatures. If it is not, the function reverts with an error message. If all of these checks pass, the function sets the executed flag of the transaction to true, indicating that the transaction has been executed. It then attempts to execute the transaction by calling the call function on the destination address with the specified value and data. If the transaction execution is successful, the function emits a TransactionExecuted event with the ID of the executed transaction and the address of the function caller. Complete contract Here is the full contract in case you missed some part or don't want to code along: // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; contract Multisig { uint256 private _requiredSignatures; address[] private _owners; struct Transaction { address to; uint256 value; bytes data; bool executed; mapping(address => bool) signatures; } Transaction[] private _transactions; event TransactionCreated(uint256 transactionId, address to, uint256 value, bytes data); event TransactionSigned(uint256 transactionId, address signer); event TransactionExecuted(uint256 transactionId, address executer); constructor(address[] memory owners, uint256 requiredSignatures) { require(owners.length > 0, "At least one owner required"); require(requiredSignatures > 0 && requiredSignatures <= owners.length, "Invalid number of required signatures"); _owners = owners; _requiredSignatures = requiredSignatures; } function submitTransaction(address to, uint256 value, bytes memory data) public { require(isOwner(msg.sender), "Not an owner!"); require(to != address(0), "Invalid destination address"); require(value >= 0, "Invalid value"); uint256 transactionId = _transactions.length; _transactions.push(); Transaction storage transaction = _transactions[transactionId]; transaction.to = to; transaction.value = value; transaction.data = data; transaction.executed = false; emit TransactionCreated(transactionId, to, value, data); } function signTransaction(uint256 transactionId) public { require(transactionId < _transactions.length, "Invalid transaction ID"); Transaction storage transaction = _transactions[transactionId]; require(!transaction.executed, "Transaction already executed"); require(isOwner(msg.sender), "Only owners can sign transactions"); require(!transaction.signatures[msg.sender], "Transaction already signed by this owner"); transaction.signatures[msg.sender] = true; emit TransactionSigned(transactionId, msg.sender); if(countSignatures(transaction) == _requiredSignatures) { executeTransaction(transactionId); } } function executeTransaction(uint256 transactionId) private { require(transactionId < _transactions.length, "Invalid transaction ID"); Transaction storage transaction = _transactions[transactionId]; require(!transaction.executed, "Transaction already executed"); require(countSignatures(transaction) >= _requiredSignatures, "Insufficient valid signatures"); transaction.executed = true; (bool success,) = transaction.to.call{value: transaction.value}(transaction.data); require(success, "Transaction execution failed"); emit TransactionExecuted(transactionId, msg.sender); } // HELPERS function isOwner(address account) public view returns (bool) { for (uint256 i = 0; i < _owners.length; i++) { if (_owners[i] == account) { return true; } } return false; } function countSignatures(Transaction storage transaction) private view returns (uint256) { uint256 count = 0; for (uint256 i = 0; i < _owners.length; i++) { if (transaction.signatures[_owners[i]]) { count++; } } return count; } function getTransaction(uint256 transactionId) public view returns (address, uint256, bytes memory, bool, uint256) { require(transactionId < _transactions.length, "Invalid transaction ID"); Transaction storage transaction = _transactions[transactionId]; return (transaction.to, transaction.value, transaction.data, transaction.executed, countSignatures(transaction)); } function getOwners() public view returns(address[] memory) { return _owners; } function getRequiredSignatures() public view returns(uint256) { return _requiredSignatures; } receive() external payable {} } Deployment and testing Watch my video on my YouTube channel on this exact topic to learn more about deploying and testing the contract. We go in-depth while testing this contract. Conclusion In this article, you learned how you can create your own multi-sig contract using Solidity. Two more articles are remaining in this series where we will be creating a contract factory around this contract and finally a dApp to let anyone deploy their own multi-sig wallet.

Writing a crowdfunding smart contract on EVM chains using Solidity

Writing a crowdfunding smart contract on EVM chains using Solidity

Atharva Deosthale

To better write Solidity code, creating projects with some utility is often advised. In this article, we will write a crowdfunding smart contract that allows the contract deployer to gather funds for an initiative they wish to raise. For this contract, we will input the deadline and target funds through the constructor by the contract deployer. When the deadline is crossed, the fund will be disabled, and nobody can fund the contract. At that point, if the target funds are achieved, the owner can withdraw all the funds. If not, the funders can get their funding back, as the target funds were not achieved until the provided deadline. In such a case, the contract owner cannot withdraw the funds. If the target funds are achieved before the deadline, the contract can continue receiving funds until the deadline. However, the owner can withdraw the funds before the deadline. If the owner chooses to withdraw before the deadline, the fund is closed and won't accept any further funding. Now that we have taken a brief look at how the contract will work let's start working on the contract! I've got you covered if you prefer video tutorials over written ones! I've uploaded a video on the same topic, and we deploy and test the features in the contract there. Watch the video now! Setting up the development environment Creating a hardhat project We will use hardhat in this tutorial to compile and manage our contract. Navigate to a safe directory, and run the following command to initialize the hardhat project initialization wizard: npx hardhat This should load up the initializer. Choose your configuration; in this case, I'll be choosing JavaScript (although we won't be writing scripts in this tutorial, although if you wish to do so, this choice might be important for you). Finally, choose to install the dependencies for your hardhat project. Creating the contract file Hardhat creates a file named Lock.sol within the contracts folder with some example code for you. You can delete the file and create a new file named Crowdfunding.sol. Then you can initialize a simple contract. // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; contract Crowdfunding { } Writing the contract Defining the states Our contract will have states that will store important data about the funders and the contract owner. mapping(address => uint256) public funders; uint256 public deadline; uint256 public targetFunds; string public name; address public owner; bool public fundsWithdrawn; Let's look at the purpose of each state variable: funders: a mapping that stores the amount of funds each address has contributed. deadline: the date and time at which funding will no longer be accepted. targetFunds: the amount of funds that the project hopes to raise. name: the name of the project being funded. owner: the address of the owner of the contract. fundsWithdrawn: a flag that is set to true when the owner withdraws the funds. Defining the events Our contract will emit events that other applications can listen to and track any changes in the contract: event Funded(address _funder, uint256 _amount); event OwnerWithdraw(uint256 _amount); event FunderWithdraw(address _funder, uint256 _amount); Let's look at the purpose of each event: Funded: triggered when someone contributes funds to the contract. It takes two parameters- the address of the funder and the amount contributed. OwnerWithdraw: triggered when the owner withdraws the funds from the contract. It takes one argument: the amount of funds withdrawn. FunderWithdraw: triggered when a funder withdraws funds from the contract. It takes two arguments: the address of the funder and the amount of funds withdrawn. Writing the constructor The following is the code for the constructor of this contract: constructor(string memory _name, uint256 _targetFunds, uint256 _deadline) { owner = msg.sender; name = _name; targetFunds = _targetFunds; deadline = _deadline; } The constructor is called when the contract is deployed. It takes three arguments: the project's name, the target funds, and the deadline. It sets the owner of the contract to the address that deployed the contract and sets the project name, target funds, and deadline. Writing the helper functions These helper functions will help us get the current status of the contract. Following is the code for the isFundEnabled() function: function isFundEnabled() public view returns(bool) { if (block.timestamp > deadline || fundsWithdrawn) { return false; } else { return true; } } This function is a helper function that returns a Boolean value indicating if funding is still enabled. It checks if the current block timestamp is greater than the deadline or if the funds have been withdrawn. The following is the code for isFundSuccess() function: function isFundSuccess() public view returns(bool) { if(address(this).balance >= targetFunds || fundsWithdrawn) { return true; } else { return false; } } This function is a helper function that returns a Boolean value indicating if the funding goal has been reached. It checks if the contract balance is greater than or equal to the target funds or if the funds have been withdrawn. Writing the fund() function The following code is for the fund() function: function fund() public payable { require(isFundEnabled() == true, "Funding is now disabled!"); funders[msg.sender] += msg.value; emit Funded(msg.sender, msg.value); } The fund() function allows someone to contribute funds to the contract. It has a require statement that checks if funding is still enabled before allowing the funds to be contributed. If funding is enabled, the function adds the amount of the contribution to the funder's total in the mapping and triggers the Funded event. Writing the withdrawOwner() function The following code is for the withdrawOwner() function: function withdrawOwner() public { require(msg.sender == owner, "Not authorized!"); require(isFundSuccess() == true, "Cannot withdraw!"); uint256 amountToSend = address(this).balance; (bool success,) = msg.sender.call{value: amountToSend}(""); require(success, "unable to send!"); fundsWithdrawn = true; emit OwnerWithdraw(amountToSend); } The withdrawOwner() function allows the contract owner to withdraw the funds from the contract. It has a require statement that checks if the msg.sender is the owner and if the funding goal has been reached before allowing the funds to be withdrawn. If the funding goal has been reached and the msg.sender is the owner, the function sends the balance of the contract to the owner address and triggers the OwnerWithdraw event. Writing the withdrawFunder() function The following code is for the withdrawFunder() function: function withdrawFunder() public { require(isFundEnabled() == false && isFundSuccess() == false, "Not eligible!"); uint256 amountToSend = funders[msg.sender]; (bool success,) = msg.sender.call{value: amountToSend}(""); require(success, "unable to send!"); funders[msg.sender] = 0; emit FunderWithdraw(msg.sender, amountToSend); } The withdrawFunder() function allows a funder to withdraw funds from the contract. It has a require statement that checks if funding is still enabled and if the funding goal has been reached before allowing the funds to be withdrawn. If funding is not enabled and the funding goal has not been reached, the function sends the funder's contribution to the funder address and triggers the FunderWithdraw event. Complete code The following is the complete code for the Crowdfunding contract: // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; contract Crowdfunding { mapping(address => uint256) public funders; uint256 public deadline; uint256 public targetFunds; string public name; address public owner; bool public fundsWithdrawn; event Funded(address _funder, uint256 _amount); event OwnerWithdraw(uint256 _amount); event FunderWithdraw(address _funder, uint256 _amount); constructor(string memory _name, uint256 _targetFunds, uint256 _deadline) { owner = msg.sender; name = _name; targetFunds = _targetFunds; deadline = _deadline; } function fund() public payable { require(isFundEnabled() == true, "Funding is now disabled!"); funders[msg.sender] += msg.value; emit Funded(msg.sender, msg.value); } function withdrawOwner() public { require(msg.sender == owner, "Not authorized!"); require(isFundSuccess() == true, "Cannot withdraw!"); uint256 amountToSend = address(this).balance; (bool success,) = msg.sender.call{value: amountToSend}(""); require(success, "unable to send!"); fundsWithdrawn = true; emit OwnerWithdraw(amountToSend); } function withdrawFunder() public { require(isFundEnabled() == false && isFundSuccess() == false, "Not eligible!"); uint256 amountToSend = funders[msg.sender]; (bool success,) = msg.sender.call{value: amountToSend}(""); require(success, "unable to send!"); funders[msg.sender] = 0; emit FunderWithdraw(msg.sender, amountToSend); } // Helper functions, although public function isFundEnabled() public view returns(bool) { if (block.timestamp > deadline || fundsWithdrawn) { return false; } else { return true; } } function isFundSuccess() public view returns(bool) { if(address(this).balance >= targetFunds || fundsWithdrawn) { return true; } else { return false; } } } Conclusion This article taught us how to write your crowdfunding contract using Solidity. You can now also wrap this around a contract factory and enable others to deploy these contracts. But that's for another tutorial. As mentioned, you can check out the video tutorial on the same topic on my YouTube channel if you want me to test this contract or if you prefer video tutorials.

Writing an ERC-1155 token from scratch using Solidity

Writing an ERC-1155 token from scratch using Solidity

Atharva Deosthale

We hear about fungible (ERC-20) and non-fungible (ERC-721) tokens daily. But what if I told you that there exists a semi-fungible token? In a semi-fungible token contract, you can find tokens with copies (just like fungible tokens) and multiple tokens in the contract (just like non-fungible tokens). These semi-fungible tokens are based on the ERC-1155 standard. Semi-fungible tokens have a lot of use cases, the most popular being token-gated communities. The ERC-1155 token shines here, unlike ERC-721, because many people need to hold the access NFT, and creating many NFTs under the ERC-721 contract does not make much sense. On the other hand, if you use ERC-1155 tokens for such use cases, you can just mint a new semi-fungible token and create copies of it for people to hold. Using ERC-20 is also an option, although using ERC-1155 opens up the opportunity of creating different types of tokens representing tiers in your community. In this article, we will see how you can write an ERC-1155 token from scratch without using any libraries, and finally, we will test if it works! Prefer video tutorials? I've got you covered! Check out my video on writing an ERC-1155 token from scratch! Also, would you be interested in learning how to write an ERC-721 token from scratch? Let's start with the craziness 🚀 Setting up the development environment Creating a hardhat project We will use hardhat in this tutorial to compile and manage our contract. Navigate to a safe directory, and run the following command to initialize the hardhat project initialization wizard: npx hardhat This should load up the initializer. Choose your configuration; in this case, I'll be choosing JavaScript (although we won't be writing scripts in this tutorial, although if you wish to do so, this choice might be important for you). Finally, choose to install the dependencies for your hardhat project. Creating the contract file Hardhat creates a file named Lock.sol within the contracts folder with some example code for you. You can delete the file and create a new file named ERC1155.sol. Then you can initialize a simple contract. // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; contract ERC1155 { } Writing the contract Defining the states Our contract will have states that will store important data about the tokens and owners. Following are the states we will be using in our contract. // token id => (address => balance) mapping(uint256 => mapping(address => uint256)) internal _balances; // owner => (operator => yes/no) mapping(address => mapping(address => bool)) internal _operatorApprovals; // token id => token uri mapping(uint256 => string) internal _tokenUris; // token id => supply mapping(uint256 => uint256) public totalSupply; uint256 public nextTokenIdToMint; string public name; string public symbol; address public owner; Now, let's see what each state is responsible for storing. _balances: this internal state uses nested mapping and stores user balances for each token ID. _operatorApprovals: this internal state uses nested mapping and stores operator approvals for each wallet. These operators are allowed to transfer tokens on the owner's behalf. _tokenUris: this internal state keeps track of the token URI for each token ID. The token URIs are typically IPFS URIs that store the token's metadata, for example- image, name, description, etc. totalSupply: this state keeps track of the number of tokens minted per token ID. nextTokenIdToMint: this state keeps track of the next token ID to mint in case a new token is supposed to be minted. name: name of the collection. symbol: symbol of the collection. owner: owner of the contract. We will set it as the contract deployer once we cover the constructor part. Defining the events The ERC-1155 standard has three events. Apps can listen to these events to detect any changes in the contract state. Let's define these events in our contract. event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value); event TransferBatch( address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values ); event ApprovalForAll(address indexed account, address indexed operator, bool approved); Let's see what the above events do. TransferSingle: emitted when a single token is transferred by someone. TransferBatch: emitted when someone performs a batch transfer. ApprovalForAll: emitted when someone sets an operator. Creating the constructor The constructor will input data about the contract and then initialize all the data. The constructor function is run when the smart contract is first deployed. constructor(string memory _name, string memory _symbol) { owner = msg.sender; name = _name; symbol = _symbol; nextTokenIdToMint = 0; } In the above code, we are taking input (name and symbol of the collection) from the deployer in the form of constructor. We are then setting the owner, name, nextTokenIdToMint and symbol states. The owner of contract is set to msg.sender which is the contract deployer. Creating the balanceOf() function Let's write a function that returns the balance of a user for a given token ID. function balanceOf(address _owner, uint256 _tokenId) public view returns(uint256) { require(_owner != address(0), "Add0"); return _balances[_tokenId][_owner]; } In the above code, we are taking _owner and _tokenId as parameters and fetching the balance from the _balances mapping. Remember that both parameters are required to fetch the balance. Creating the balanceOfBatch() function One of the reason why the ERC-1155 standard is interesting is because of the use of batch operations. You can pass in a bunch of address and token IDs in the function parameters and receive the balance for the addresses for given token IDs in the form of an array. function balanceOfBatch(address[] memory _accounts, uint256[] memory _tokenIds) public view returns(uint256[] memory) { require(_accounts.length == _tokenIds.length, "accounts id length mismatch"); // create an array dynamically uint256[] memory balances = new uint256[](_accounts.length); for(uint256 i = 0; i < _accounts.length; i++) { balances[i] = balanceOf(_accounts[i], _tokenIds[i]); } return balances; } The way this function works is you pass two arrays to the function, _accounts and _tokenIds. These arrays must be of the same length! While looping through the number of elements in the array, balance will be computed for every nth element. For example if the index is 2 then balance will be calculated for address _accounts[2] for token ID _tokenIds[2]. In the above code we are doing the following: Checking if the two received arrays are equal, else revert the transaction. Creating an array balances dynamically with the same length as the arrays received. Looping through the array and fetching balances for every address for respective token IDs and adding it to the balances array. Returning the balances array. Creating the setApprovalForAll() function This function will be used to set operators that can transfer tokens on a user's behalf. function setApprovalForAll(address _operator, bool _approved) public { _operatorApprovals[msg.sender][_operator] = _approved; } In the above code, we are taking _operator and _approved as parameters and manipulating the _operatorApprovals mapping. Depending on the value of _approved the operator will be set as active/inactive. Inactive operators cannot transfer any tokens on the user's behalf while active operators can. Creating the isApprovedForAll() function This function will return if an operator is active for a given user. function isApprovedForAll(address _account, address _operator) public view returns(bool) { return _operatorApprovals[_account][_operator]; } In the above code, we are simply accessing the _operatorApprovals mapping and returning the status of the operator. About safe transfer functions Unlike ERC-721 tokens, we have no choice of using unsafe transfers in ERC-1155 tokens. Safe transfers are made so that the tokens don't end up in a contract that isn't meant to receive these tokens. Contracts that don't intend to receive these tokens might end up locking these tokens forever with no means of getting the tokens back. Although how does our contract knows that the recipient contract is supposed to receive ERC-1155 tokens? By checking if the recipient contract has either onERC1155Received() or onERC1155Received() functions. If the function returns the function selector as a bytes4 response, it can be concluded that the contract indeed intends to collect tokens. Now, let's create an interface so that calling those two functions on the recipient contract can be easier. Create a new file in the contracts directory named IERC1155Receiver.sol and have the following contents: // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; interface IERC1155Receiver { function onERC1155Received( address operator, address from, uint256 id, uint256 value, bytes calldata data ) external returns (bytes4); function onERC1155BatchReceived( address operator, address from, uint256[] calldata ids, uint256[] calldata values, bytes calldata data ) external returns (bytes4); } Interface only has the function declarations so that we can use these to later contact the function on the recipient contract. Now, let's import this file into our ERC1155.sol file: import "./IERC1155Receiver.sol"; Now, we can implement the safety check functions. There are private functions so only functions within the contract can call these functions: function _doSafeTransferAcceptanceCheck( address operator, address from, address to, uint256 id, uint256 amount, bytes memory data ) private { if (to.code.length > 0) { try IERC1155Receiver(to).onERC1155Received(operator, from, id, amount, data) returns (bytes4 response) { if (response != IERC1155Receiver.onERC1155Received.selector) { revert("ERC1155: ERC1155Receiver rejected tokens"); } } catch Error(string memory reason) { revert(reason); } catch { revert("ERC1155: transfer to non-ERC1155Receiver implementer"); } } } function _doSafeBatchTransferAcceptanceCheck( address operator, address from, address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data ) private { if (to.code.length > 0) { try IERC1155Receiver(to).onERC1155BatchReceived(operator, from, ids, amounts, data) returns ( bytes4 response ) { if (response != IERC1155Receiver.onERC1155BatchReceived.selector) { revert("ERC1155: ERC1155Receiver rejected tokens"); } } catch Error(string memory reason) { revert(reason); } catch { revert("ERC1155: transfer to non-ERC1155Receiver implementer"); } } } Both of the functions, _doSafeTransferAcceptanceCheck() and _doSafeBatchTransferAcceptanceCheck() do similar things so we covered them in one block. Here's what these functions do: Check if the recipient is a contract. If the recipient is a contract to.code.length > 0 will always be true. If the recipient is not a contract, no checks will be performed. We are using the interface and passing it the to address and trying to invoke the functions on the recipient contract. If intended selector is returned, the transaction will be successful or will be reverted. Creating the _transfer() function This is an internal function that will transfer user tokens without any checks, and that's why this function is internal. We will be wrapping this function around on the safeTransferFrom() and safeBatchTransferFrom() functions. The reason we are creating this function is to avoid repeating code. function _transfer(address _from, address _to, uint256[] memory _ids, uint256[] memory _amounts) internal { require(_to != address(0), "transfer to address 0"); for (uint256 i = 0; i < _ids.length; i++) { uint256 id = _ids[i]; uint256 amount = _amounts[i]; uint256 fromBalance = _balances[id][_from]; require(fromBalance >= amount, "insufficient balance for transfer"); _balances[id][_from] -= amount; _balances[id][_to] += amount; } } The above function supports only batch transfers. The way we will be doing single transfers is converting the single transfers into batch and run through this function. Let's see what is the above function is doing: Checking if the transfer is being done to address 0, if so, revert. Running a loop for the number of token IDs and performing transfers for each entry in the array. In the loop, we are manipulating balances in the _balances mapping. Creating the safeTransferFrom() function This functions will be used to transfer tokens and run safety checks: function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _amount, bytes memory _data) public { require(_from == msg.sender || isApprovedForAll(_from, msg.sender), "not authorized"); // create an array uint256[] memory ids = new uint256[](1); uint256[] memory amounts = new uint256[](1); ids[0] = _id; amounts[0] = _amount; // transfer _transfer(_from, _to, ids, amounts); emit TransferSingle(msg.sender, _from, _to, _id, _amount); // safe transfer checks _doSafeTransferAcceptanceCheck(msg.sender, _from, _to, _id, _amount, _data); } In the above code, we are running checks on whether the _from is the transaction initiator or the transaction initiator is the operator for _from. If not, we revert. Then, we are creating singleton array with only one element and we fill in the _id and _amount in the 0th index. We then run the _transfer() function, emit the TransferSingle event, and then run the safety checks using the _doSafeTransferAcceptanceCheck() function. You might be wondering- why is the safety checks run after transferring the tokens? Won't it violate the entire purpose and still transfer the tokens? No, that's not how it works. We are transferring tokens beforehand because then the recipient contract can do their checks and run operations when the functions on the recipient contract are invoked. Although, if there is no response from the contract, transaction gets reverted, meaning the state on the blockchain will not change and the transaction never took place. Creating the safeBatchTransferFrom() function This function is pretty similar to safeTransferFrom() function, except here we don't need to create dynamic arrays, we simply pass everything to _transfer() function and emit the TransferBatch event: function safeBatchTransferFrom(address _from, address _to, uint256[] memory _ids, uint256[] memory _amounts, bytes memory _data) public { require(_from == msg.sender || isApprovedForAll(_from, msg.sender), "not authorized"); require(_ids.length == _amounts.length, "length mismatch"); _transfer(_from, _to, _ids, _amounts); emit TransferBatch(msg.sender, _from, _to, _ids, _amounts); _doSafeBatchTransferAcceptanceCheck(msg.sender, _from, _to, _ids, _amounts, _data); } Creating the mintTo() function This function is not required by the ERC-1155 standard, although it's essential to add in our contract as this function will help us mint new tokens into circulation. function mintTo(address _to, uint256 _tokenId, string memory _uri, uint256 _amount) public { require(owner == msg.sender, "not authorized"); uint256 tokenIdToMint; if (_tokenId == type(uint256).max) { tokenIdToMint = nextTokenIdToMint; nextTokenIdToMint += 1; _tokenUris[tokenIdToMint] = _uri; } else { require(_tokenId < nextTokenIdToMint, "invalid id"); tokenIdToMint = _tokenId; } _balances[tokenIdToMint][_to] += _amount; totalSupply[tokenIdToMint] += _amount; emit TransferSingle(msg.sender, address(0), _to, _tokenId, _amount); } The above code is hard to digest when looked at initially, let's look at what it does. We are checking if the function caller is the owner of the contract, if not, we revert as we don't want anyone else to mint tokens on this contract. We are checking if the token ID provided in the function parameter is the max of uint256 type. If it is, it's an indicator that we want to mint new tokens instead of minting additional supply into existing tokens. So, we take note of nextTokenIdToMint in a temporary variable, increment nextTokenIdToMint and set the token URI received in the parameter by manipulating the _tokenURIs mapping. If additional supply is intended, we check if the token ID is already minted, if not, we revert. We set tokenIdToMint to the _tokenId received in the parameters. We then manipulate balance of _to using the _balances mapping and we also change the total supply of the token ID by manipulating the totalSupply mapping. We then emit a TransferSingle event where from is address 0, which means the tokens just got minted. Creating the uri() function This function will return the token URI for any given token ID. function uri(uint256 _tokenId) public view returns(string memory) { return _tokenUris[_tokenId]; } Completed code If you cannot fix the pieces together, here's the complete code for the contract: // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; import "./IERC1155Receiver.sol"; contract ERC1155 { // token id => (address => balance) mapping(uint256 => mapping(address => uint256)) internal _balances; // owner => (operator => yes/no) mapping(address => mapping(address => bool)) internal _operatorApprovals; // token id => token uri mapping(uint256 => string) internal _tokenUris; // token id => supply mapping(uint256 => uint256) public totalSupply; uint256 public nextTokenIdToMint; string public name; string public symbol; address public owner; event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value); event TransferBatch( address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values ); event ApprovalForAll(address indexed account, address indexed operator, bool approved); constructor(string memory _name, string memory _symbol) { owner = msg.sender; name = _name; symbol = _symbol; nextTokenIdToMint = 0; } function balanceOf(address _owner, uint256 _tokenId) public view returns(uint256) { require(_owner != address(0), "Add0"); return _balances[_tokenId][_owner]; } function balanceOfBatch(address[] memory _accounts, uint256[] memory _tokenIds) public view returns(uint256[] memory) { require(_accounts.length == _tokenIds.length, "accounts id length mismatch"); // create an array dynamically uint256[] memory balances = new uint256[](_accounts.length); for(uint256 i = 0; i < _accounts.length; i++) { balances[i] = balanceOf(_accounts[i], _tokenIds[i]); } return balances; } function setApprovalForAll(address _operator, bool _approved) public { _operatorApprovals[msg.sender][_operator] = _approved; } function isApprovedForAll(address _account, address _operator) public view returns(bool) { return _operatorApprovals[_account][_operator]; } function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _amount, bytes memory _data) public { require(_from == msg.sender || isApprovedForAll(_from, msg.sender), "not authorized"); // create an array uint256[] memory ids = new uint256[](1); uint256[] memory amounts = new uint256[](1); ids[0] = _id; amounts[0] = _amount; // transfer _transfer(_from, _to, ids, amounts); emit TransferSingle(msg.sender, _from, _to, _id, _amount); // safe transfer checks _doSafeTransferAcceptanceCheck(msg.sender, _from, _to, _id, _amount, _data); } function safeBatchTransferFrom(address _from, address _to, uint256[] memory _ids, uint256[] memory _amounts, bytes memory _data) public { require(_from == msg.sender || isApprovedForAll(_from, msg.sender), "not authorized"); require(_ids.length == _amounts.length, "length mismatch"); _transfer(_from, _to, _ids, _amounts); emit TransferBatch(msg.sender, _from, _to, _ids, _amounts); _doSafeBatchTransferAcceptanceCheck(msg.sender, _from, _to, _ids, _amounts, _data); } function uri(uint256 _tokenId) public view returns(string memory) { return _tokenUris[_tokenId]; } function mintTo(address _to, uint256 _tokenId, string memory _uri, uint256 _amount) public { require(owner == msg.sender, "not authorized"); uint256 tokenIdToMint; if (_tokenId == type(uint256).max) { tokenIdToMint = nextTokenIdToMint; nextTokenIdToMint += 1; _tokenUris[tokenIdToMint] = _uri; } else { require(_tokenId < nextTokenIdToMint, "invalid id"); tokenIdToMint = _tokenId; } _balances[tokenIdToMint][_to] += _amount; totalSupply[tokenIdToMint] += _amount; emit TransferSingle(msg.sender, address(0), _to, _tokenId, _amount); } // INTERNAL FUNCTIONS function _transfer(address _from, address _to, uint256[] memory _ids, uint256[] memory _amounts) internal { require(_to != address(0), "transfer to address 0"); for (uint256 i = 0; i < _ids.length; i++) { uint256 id = _ids[i]; uint256 amount = _amounts[i]; uint256 fromBalance = _balances[id][_from]; require(fromBalance >= amount, "insufficient balance for transfer"); _balances[id][_from] -= amount; _balances[id][_to] += amount; } } function _doSafeTransferAcceptanceCheck( address operator, address from, address to, uint256 id, uint256 amount, bytes memory data ) private { if (to.code.length > 0) { try IERC1155Receiver(to).onERC1155Received(operator, from, id, amount, data) returns (bytes4 response) { if (response != IERC1155Receiver.onERC1155Received.selector) { revert("ERC1155: ERC1155Receiver rejected tokens"); } } catch Error(string memory reason) { revert(reason); } catch { revert("ERC1155: transfer to non-ERC1155Receiver implementer"); } } } function _doSafeBatchTransferAcceptanceCheck( address operator, address from, address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data ) private { if (to.code.length > 0) { try IERC1155Receiver(to).onERC1155BatchReceived(operator, from, ids, amounts, data) returns ( bytes4 response ) { if (response != IERC1155Receiver.onERC1155BatchReceived.selector) { revert("ERC1155: ERC1155Receiver rejected tokens"); } } catch Error(string memory reason) { revert(reason); } catch { revert("ERC1155: transfer to non-ERC1155Receiver implementer"); } } } } Testing the contract Typically to deploy and test the contract functions, you'll need to write scripts and add private keys to your environment variables to deploy and interact with the contract. However, thirdweb deploy provides an easy and interactive workflow for deploying your contracts on the blockchain without exposing your private keys - and it's FREE! You don't need any additional setup- run the following command in the terminal to begin deploying the contract. npx thirdweb@latest deploy This command will do all the hard work- compiling the contract and uploading the data to IPFS, and then you will be redirected to your browser to pass in the parameters to deploy the contract finally. As you can see, thirdweb detects that our contract is an ERC-1155 contract with the extensions ERC1155Enumerable and ERC1155Mintable. Enter your desired _name and _symbol. Choose your desired network and click on deploy now. This should initiate a transaction on your wallet provider (for ex, MetaMask, WalletConnect, etc.), and on approval, your transaction will be deployed on the blockchain. No private keys involved. After deploying, you can go to the NFTs tab on the top menu and click on Mint. Then you can see a drawer like this: You can enter all the data, and thirdweb will automatically upload all your assets to IPFS, generate a JSON file, upload it to IPFS and invoke the mintTo() function for you! And all of this is FREE! For example, I just used this to mint my favourite Naruto character- Madara Uchiha. You can now test the other functionality by calling the contract functions in the Explorer tab. Check out my video if you want to see me test it a bit more. Conclusion In this article, we saw how to write an ERC-1155 token from scratch. I'd say every Solidity developer must do this at least once, as this makes you understand the basics of Solidity a lot better than the regular to-do list contracts. If you have any suggestions or questions, feel free to leave them in the comments below or contact me on other platforms.

Writing an ERC-721 token from scratch using Solidity

Writing an ERC-721 token from scratch using Solidity

Atharva Deosthale

Non-fungible tokens (NFTs) have gained much popularity in recent months. Every day, we see new collections popping up in the space, and some of them even have cool utilities such as community access, software access, and so on. Deploying smart contracts for such tokens is not a difficult task. You can find many tools, for example, thirdweb, that allows you to deploy such a contract with just a few clicks. You can even use libraries like OpenZeppelin that assist you with libraries while programming such a contract. This tutorial will explore writing an ERC-721 token from scratch using Solidity. To create an ERC-721 token, we will need to use Solidity, the primary programming language for writing smart contracts on Ethereum. This tutorial will teach you how to set up your development environment and deploy your ERC-721 token to the blockchain. By the end of this tutorial, you will have a solid understanding of how to create an ERC-721 token from scratch using Solidity. We will follow the official ethereum.org ERC-721 documentation and create an implementation around it. I'd recommend opening that link in a browser tab and constantly referring to it when we create a new function. If you prefer video tutorials, I've got you covered; check out my video on writing an ERC-721 token from scratch using Solidity on my YouTube channel. Also, would you be interested in writing an ERC-20 token from scratch using Solidity? So, let's start with the craziness 🚀 Setting up the development environment Creating a hardhat project We will use hardhat in this tutorial to compile and manage our contract. Navigate to a safe directory, and run the following command to initialize the hardhat project initialization wizard: npx hardhat This should load up the initializer. Choose your configuration; in this case, I'll be choosing JavaScript (although we won't be writing scripts in this tutorial, although if you wish to do so, this choice might be important for you). Finally, choose to install the dependencies for your hardhat project. Creating the contract file Hardhat creates a file named Lock.sol within the contracts folder with some example code for you. You can delete the file and create a new file named ERC721.sol. Then you can initialize a simple contract. // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; contract ERC721 { } Writing the contract Defining the states Our contract will have states that will store important data about the tokens and owners. Following are the states we will be using in our contract. string public name; string public symbol; uint256 public nextTokenIdToMint; address public contractOwner; // token id => owner mapping(uint256 => address) internal _owners; // owner => token count mapping(address => uint256) internal _balances; // token id => approved address mapping(uint256 => address) internal _tokenApprovals; // owner => (operator => yes/no) mapping(address => mapping(address => bool)) internal _operatorApprovals; // token id => token uri mapping(uint256 => string) _tokenUris; Now, let's look at what each state is responsible for storing. name: name of the token, can be read by apps to know the collection's name. symbol: symbol for the token, just like an ERC-20 token. nextTokenIdToMint: indicates the token ID for the next NFT that will be minted. This state keeps track of the next token ID and ensures no two NFTs have the same token ID. contractOwner: this will be set to msg.sender in the constructor. We are using this to restrict token minting to the contract owner. _owners: this internal state keeps track of token IDs and their owners. _balances: this internal state keeps track of how many tokens an address owns. _tokenApprovals: this internal state keeps track of any addresses allowed to manage someone's else tokens. This approval is based on token ID and works best when you don't want someone to manage all your tokens but specific tokens. _operatorApprovals: this internal state keeps track of all the operators for a wallet. These operators can manage all the tokens owned by an address for that specific ERC-721 contract. _tokenUris: this state keeps track of all the token URIs that lead to a JSON file with all the NFT metadata. This is not a part of the standard and is optional. However, we cover it because most NFT collections have metadata on their NFTs. Defining the events We will use the events that are specified in the ERC-721 documentation. Apps can listen to these events and track any changes. Let's define these events beforehand. event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId); event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); Creating the constructor Now, we will create a constructor to initialize values when the contract is deployed. constructor(string memory _name, string memory _symbol) { name = _name; symbol = _symbol; nextTokenIdToMint = 0; contractOwner = msg.sender; } In the above code, we are setting the name and symbol of the contract as described by the contract deployer; we are also initializing nextTokenIdToMint it as 0, making the first token ID to be 0, and finally, we're setting the contract owner value as msg.sender Creating the balanceOf() function Let's create the balanceOf() function that will help us get the wallet's token balance (number of NFTs owned). function balanceOf(address _owner) public view returns(uint256) { require(_owner != address(0), "!Add0"); return _balances[_owner]; } The above function will access the _balances mapping and return the balance of the passed wallet address. Creating the ownerOf() function Let's create the ownerOf() function that will help us get the wallet address of the owner of a specific token ID. function ownerOf(uint256 _tokenId) public view returns(address) { return _owners[_tokenId]; } If an invalid token ID is passed, a zero address will be returned as the token is yet to be minted. About safeTransferFrom() function If you look at the standards of an ERC721 token, you will see two types of transfer functions, transferFrom() and safeTransferFrom(). The transferFrom() function is pretty straightforward; it allows you to send NFTs from one address to another. However, the safeTransferFrom() function does more than that; it checks if the recipient can receive ERC-721 tokens and if not, the transaction is reverted. Many people accidentally send their NFTs to contracts that aren't meant to receive these tokens, and then they are locked up forever with no means to retrieve them. The safeTransferFrom() function is a way to implement safe transfers. The way to check whether the recipient contract can receive ERC-721 tokens is by checking if they have a onERC721Received() function, which automatically means they mean to receive ERC-721 tokens. If the function does not exist on the recipient contract, the transaction is reverted to save the token from being locked up forever. Preparing to create safeTransferFrom() function Let's first create an unsafe internal function to transfer the token, no questions asked. We can then wrap this function around other functions and do some checks. // unsafe transfer function _transfer(address _from, address _to, uint256 _tokenId) internal { require(ownerOf(_tokenId) == _from, "!Owner"); require(_to != address(0), "!ToAdd0"); delete _tokenApprovals[_tokenId]; _balances[_from] -= 1; _balances[_to] += 1; _owners[_tokenId] = _to; emit Transfer(_from, _to, _tokenId); } The above function will check if the _from is the owner of _tokenId and proceed to delete the token approvals and transfer the token to the _to address by changing the _balances and _owners mappings. Finally, we emit a Transfer event so that apps can listen to any transfers made on the contract. Now, let's create an interface for the onERC721Received() function so that we can use the interface to access the function in the recipient contract. Create a new file in the contracts folder named IERC721Receiver.sol and have the following contents. // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; interface IERC721Receiver { function onERC721Received( address operator, address from, uint256 tokenId, bytes calldata data ) external returns (bytes4); } The above code is just a simple interface that defines the onERC721Received() function. Now let's go back to our main ERC721.sol file. Now let's create an internal function that checks whether the recipient contract has the onERC721Received() function. function _checkOnERC721Received( address from, address to, uint256 tokenId, bytes memory data ) private returns (bool) { // check if to is an contract, if yes, to.code.length will always > 0 if (to.code.length > 0) { try IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, data) returns (bytes4 retval) { return retval == IERC721Receiver.onERC721Received.selector; } catch (bytes memory reason) { if (reason.length == 0) { revert("ERC721: transfer to non ERC721Receiver implementer"); } else { /// @solidity memory-safe-assembly assembly { revert(add(32, reason), mload(reason)) } } } } else { return true; } } The above code looks pretty complicated because it is. Most of the contracts use the above implementation to check whether the function exists on the recipient contract. First, we check if the recipient is a contract (such checks are not carried out on wallet accounts). Then we try to access the onERC721Received() function on the recipient contract and check if we get a solidity selector back. If anything goes wrong or the recipient doesn't implement the function, we revert the transaction, preventing any token transfers from materializing. Creating the safeTransferFrom() function Finally, now let's implement the safeTransferFrom() function. function safeTransferFrom(address _from, address _to, uint256 _tokenId) public payable { safeTransferFrom(_from, _to, _tokenId, ""); } function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes memory _data) public payable { require(ownerOf(_tokenId) == msg.sender || _tokenApprovals[_tokenId] == msg.sender || _operatorApprovals[ownerOf(_tokenId)][msg.sender], "!Auth"); _transfer(_from, _to, _tokenId); // trigger func check require(_checkOnERC721Received(_from, _to, _tokenId, _data), "!ERC721Implementer"); } We are using function overloading in the above code by creating the same function name with different parameters. One of them has an extra _data parameter. The function without the _data parameter simply calls the one with the _data parameter but with empty data. So finally, we end up inside the one with _data as the parameter. In the function, we check if the transaction initiator is either the owner of the token, approved for the token ID or is an operator for the token owner. If not, we will revert the transaction. Then we perform the transfer. After the states have updated, we trigger the _checkOnERC721Received() function. If it fails, the transaction is reverted, the state changes never materialize, and the transfer never takes place. Creating the transferFrom() function If the user understands the risk and wants to transfer the tokens, they can use the transferFrom() function. It's the same function without any contract checks. function transferFrom(address _from, address _to, uint256 _tokenId) public payable { // unsafe transfer without onERC721Received, used for contracts that dont implement require(ownerOf(_tokenId) == msg.sender || _tokenApprovals[_tokenId] == msg.sender || _operatorApprovals[ownerOf(_tokenId)][msg.sender], "!Auth"); _transfer(_from, _to, _tokenId); } Creating the approve() function The approve() function will let someone transfer a specific token owned by you on your behalf. function approve(address _approved, uint256 _tokenId) public payable { require(ownerOf(_tokenId) == msg.sender, "!Owner"); _tokenApprovals[_tokenId] = _approved; emit Approval(ownerOf(_tokenId), _approved, _tokenId); } In the above code, we are checking if the owner of the specified token ID is the transaction initiator; if not, we revert. Then we update the _tokenApprovals mapping and emit the Approval event. Creating the setApprovalForAll() function The setApprovalForAll() function allows you to authorize someone to manage all the NFTs on the contract. function setApprovalForAll(address _operator, bool _approved) public { _operatorApprovals[msg.sender][_operator] = _approved; emit ApprovalForAll(msg.sender, _operator, _approved); } This function requires no check; we can directly make changes to _operatorApprovals and emit ApprovalForAll event. Creating the getApproved() and isApprovedForAll() functions These functions will return the token or operator approval status. function getApproved(uint256 _tokenId) public view returns (address) { return _tokenApprovals[_tokenId]; } function isApprovedForAll(address _owner, address _operator) public view returns (bool) { return _operatorApprovals[_owner][_operator]; } In the above code, we are simply accessing the mappings _tokenApprovals and _operatorApprovals and returning the approval status. Creating the mintTo() function This function will allow the contract owner to mint NFTs into the collection. function mintTo(address _to, string memory _uri) public { require(contractOwner == msg.sender, "!Auth"); _owners[nextTokenIdToMint] = _to; _balances[_to] += 1; _tokenUris[nextTokenIdToMint] = _uri; emit Transfer(address(0), _to, nextTokenIdToMint); nextTokenIdToMint += 1; } In the above function, we check if the transaction initiator is the contract owner; if not, we revert the transaction. If it is the owner, we proceed with manipulating the _owners, _balances and _tokenUris mappings. Note how we use the nextTokenIdToMint here. Finally, we emit a Transfer event with from address as zero address event and increment nextTokenIdToMint by 1. Creating the tokenURI() and totalSupply() functions function tokenURI(uint256 _tokenId) public view returns(string memory) { return _tokenUris[_tokenId]; } function totalSupply() public view returns(uint256) { return nextTokenIdToMint; } The tokenURI function is used to get a token URI for any specific token ID to access metadata for any given token ID. The totalSupply function returns the number of NFTs minted. Since we start from zero, the total supply at any point happens to be nextTokenIdToMint. If you are starting from other than token ID 0, I'd recommend changing this function. Final code Here's the final code for the ERC-721 contract! First, ERC721.sol: // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; import './IERC721Receiver.sol'; contract ERC721 { string public name; string public symbol; uint256 public nextTokenIdToMint; address public contractOwner; // token id => owner mapping(uint256 => address) internal _owners; // owner => token count mapping(address => uint256) internal _balances; // token id => approved address mapping(uint256 => address) internal _tokenApprovals; // owner => (operator => yes/no) mapping(address => mapping(address => bool)) internal _operatorApprovals; // token id => token uri mapping(uint256 => string) _tokenUris; event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId); event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); constructor(string memory _name, string memory _symbol) { name = _name; symbol = _symbol; nextTokenIdToMint = 0; contractOwner = msg.sender; } function balanceOf(address _owner) public view returns(uint256) { require(_owner != address(0), "!Add0"); return _balances[_owner]; } function ownerOf(uint256 _tokenId) public view returns(address) { return _owners[_tokenId]; } function safeTransferFrom(address _from, address _to, uint256 _tokenId) public payable { safeTransferFrom(_from, _to, _tokenId, ""); } function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes memory _data) public payable { require(ownerOf(_tokenId) == msg.sender || _tokenApprovals[_tokenId] == msg.sender || _operatorApprovals[ownerOf(_tokenId)][msg.sender], "!Auth"); _transfer(_from, _to, _tokenId); // trigger func check require(_checkOnERC721Received(_from, _to, _tokenId, _data), "!ERC721Implementer"); } function transferFrom(address _from, address _to, uint256 _tokenId) public payable { // unsafe transfer without onERC721Received, used for contracts that dont implement require(ownerOf(_tokenId) == msg.sender || _tokenApprovals[_tokenId] == msg.sender || _operatorApprovals[ownerOf(_tokenId)][msg.sender], "!Auth"); _transfer(_from, _to, _tokenId); } function approve(address _approved, uint256 _tokenId) public payable { require(ownerOf(_tokenId) == msg.sender, "!Owner"); _tokenApprovals[_tokenId] = _approved; emit Approval(ownerOf(_tokenId), _approved, _tokenId); } function setApprovalForAll(address _operator, bool _approved) public { _operatorApprovals[msg.sender][_operator] = _approved; emit ApprovalForAll(msg.sender, _operator, _approved); } function getApproved(uint256 _tokenId) public view returns (address) { return _tokenApprovals[_tokenId]; } function isApprovedForAll(address _owner, address _operator) public view returns (bool) { return _operatorApprovals[_owner][_operator]; } function mintTo(address _to, string memory _uri) public { require(contractOwner == msg.sender, "!Auth"); _owners[nextTokenIdToMint] = _to; _balances[_to] += 1; _tokenUris[nextTokenIdToMint] = _uri; emit Transfer(address(0), _to, nextTokenIdToMint); nextTokenIdToMint += 1; } function tokenURI(uint256 _tokenId) public view returns(string memory) { return _tokenUris[_tokenId]; } function totalSupply() public view returns(uint256) { return nextTokenIdToMint; } // INTERNAL FUNCTIONS function _checkOnERC721Received( address from, address to, uint256 tokenId, bytes memory data ) private returns (bool) { // check if to is an contract, if yes, to.code.length will always > 0 if (to.code.length > 0) { try IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, data) returns (bytes4 retval) { return retval == IERC721Receiver.onERC721Received.selector; } catch (bytes memory reason) { if (reason.length == 0) { revert("ERC721: transfer to non ERC721Receiver implementer"); } else { /// @solidity memory-safe-assembly assembly { revert(add(32, reason), mload(reason)) } } } } else { return true; } } // unsafe transfer function _transfer(address _from, address _to, uint256 _tokenId) internal { require(ownerOf(_tokenId) == _from, "!Owner"); require(_to != address(0), "!ToAdd0"); delete _tokenApprovals[_tokenId]; _balances[_from] -= 1; _balances[_to] += 1; _owners[_tokenId] = _to; emit Transfer(_from, _to, _tokenId); } } The following is the code for IERC721Receiver.sol: // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; interface IERC721Receiver { function onERC721Received( address operator, address from, uint256 tokenId, bytes calldata data ) external returns (bytes4); } Testing the contract Typically to deploy and test the contract functions, you'll need to write scripts and add private keys to your environment variables to deploy and interact with the contract. However, thirdweb deploy provides an easy and interactive workflow for deploying your contracts on the blockchain without exposing your private keys - and it's FREE! You don't need any additional setup- run the following command in the terminal to begin deploying the contract. npx thirdweb@latest deploy This command will do all the hard work- compiling the contract and uploading the data to IPFS, and then you will be redirected to your browser to pass in the parameters to deploy the contract finally. As you can see, thirdweb detects that our contract is an ERC-721 contract with the capability of checking the supply (using totalSupply() function), and also it has the capability to mint tokens (using mintTo() function). Enter your desired _name and _symbol. Choose your desired network and click on deploy now. This should initiate a transaction on your wallet provider (for ex, MetaMask, WalletConnect, etc.), and on approval, your transaction will be deployed on the blockchain. No private keys involved. After deploying, you can go to the NFTs tab on the top menu and click on Mint. Then you can see a drawer like this: You can enter all the data, and thirdweb will automatically upload all your assets to IPFS, generate a JSON file, upload it to IPFS and invoke the mintTo() function for you! And all of this is FREE! For example, I just used this to mint my favourite Naruto character- Madara Uchiha. You can now go to the Explorer tab and directly interact with the contract functions. Check out my video if you want to see me test this a bit more! Conclusion In this article, we saw how to write an ERC-721 token from scratch. I'd say every Solidity developer must do this at least once, as this makes you understand the basics of Solidity a lot better than the regular to-do list contracts. If you have any suggestions or questions, feel free to leave them in the comments below or contact me on other platforms.

Writing an ERC-20 token from scratch using Solidity

Writing an ERC-20 token from scratch using Solidity

Atharva Deosthale

Since token standards started being well-known in the community, people have come up with different tokens and used them for many things- gaming, custom currency, and much more. Creating an ERC-20 token is not a difficult task. You can either use toolings like thirdweb, which allows you to deploy such tokens in no time without any code, or OpenZeppelin, which provides you with libraries to assist you in creating your token with as less coding as possible. But have you ever wondered how an ERC-20 token works under the hood? For most people, it doesn't matter, although, as developers, we must know how it works under the hood as we are responsible for making these things and ensuring they are as secure as possible. Creating an ERC-20 smart contract from scratch without using OpenZeppelin can be challenging but rewarding for experienced Ethereum developers. This article will create a simple ERC-20 contract that adheres to the ERC-20 standard without relying on pre-existing frameworks or libraries. If you prefer a video tutorial, I have uploaded a video about writing an ERC-20 token from scratch on my YouTube channel. About ERC-20 standard First, let's define what an ERC-20 contract is and what the ERC-20 standard entails. ERC-20 is a technical standard that defines a set of rules for implementing a token on the Ethereum blockchain. These rules include the token's name, symbol, decimal places, and the functions that must be implemented for the token to be considered ERC-20 compliant. This standard allows for the creation of interoperable tokens that can be easily exchanged on the Ethereum network. For more information about the functions and events included for a contract to be classified as an ERC-20 token, check out the official docs on ethereum.org. We will be following the standard specified in those docs in this tutorial. I'd recommend keeping that page open in a tab while following this tutorial. Now, let's dive in and make our ERC-20 token! Writing our contract Creating hardhat project We will use hardhat in this tutorial to compile and manage our contract. Navigate to a safe directory, and run the following command to initialize the hardhat project initialization wizard: npx hardhat This should load up the initializer. Choose your configuration; in this case, I'll be choosing JavaScript (although we won't be writing scripts in this tutorial, although if you wish to do so, this choice might be important for you). Finally, choose to install the dependencies for your hardhat project. Creating a smart contract Now, in the contracts folder, create a new file named ERC20.sol. This will be the smart contract file we will work on in this tutorial. Now let's proceed with writing the contract. Create the basic structure for a smart contract: // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; contract ERC20 { } Defining the states Now, let's define the states we will use in the contract- these are typically data such as user balances, token names, symbols, and other data. Some of these are listed on the ERC-20 docs- look at them so that you understand why we are adding these in the state. // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; contract ERC20 { string public name; string public symbol; uint8 public immutable decimals; uint256 public immutable totalSupply; mapping(address => uint256) _balances; // spender => (owner => no of tokens allowed) mapping(address => mapping(address => uint256)) _allowances; event Transfer(address indexed _from, address indexed _to, uint256 _value); event Approval(address indexed _owner, address indexed _spender, uint256 _value); } In the above code, we define the states necessary- token name, symbol, decimals of the token, total supply of the token, user balances, allowances and the events that can be later emitted. If you look at the ERC-20 docs, you will notice we have functions for name(), symbol(), decimals() and totalSupply(). However, we don't need to create separate functions since the variables are public and can be called a function by default. If you don't know what allowances are, you can allow someone to spend tokens on your behalf. Although, you need to approve the number of tokens beforehand. We will be creating the necessary functions later in the tutorial. Creating the constructor Let's create a constructor to initialize all the data as soon as the contract is deployed on the chain. constructor(string memory _name, string memory _symbol, uint256 _totalSupply) { name = _name; symbol = _symbol; decimals = 18; totalSupply = _totalSupply; _balances[msg.sender] = _totalSupply; } In the above code field, we create a constructor that inputs the token's name, symbol and total supply and initializes all the values. We also assign the total supply as the balance for the contract deployer so that the minted tokens aren't lost after deployment and are usable. Creating the balanceOf() function Let's create a function to get the balance of a specific wallet/contract address. This function also complies with the ERC-20 standards. function balanceOf(address _owner) public view returns(uint256) { require(_owner != address(0), "!ZA"); return _balances[_owner]; } In the above code, we check if the address provided is a zero address and revert the transaction if so. If not, we will return the balance of the user by accessing the _balances mapping. Creating the transfer() function Let's create the transfer function to help token holders transfer their tokens to other wallets/contracts. function transfer(address _to, uint256 _value) public returns(bool) { require((_balances[msg.sender] >= _value) && (_balances[msg.sender] != 0), "!Bal"); _balances[msg.sender] -= _value; _balances[_to] += _value; emit Transfer(msg.sender, _to, _value); return true; } In the above code block, we check if the transaction sender (also the token sender) has enough token balance to afford this transaction and revert if the sender has insufficient balance. Then, we access the _balances mapping and subtracting the amount from the sender and adding the same amount to the receiver. That way, the balance will reduce when sending tokens to other wallets/contracts. We then emit a Transfer() event that is part of the ERC-20 standard events. This can help apps to listen when any transfers are made on your token. Creating the transferFrom() function This function is similar to the transfer() function, although we use this function to send funds on behalf of other wallets. For this, allowances must be set by a wallet that intends to allow its tokens to be transferred by other wallets. function transferFrom(address _from, address _to, uint256 _value) public returns(bool) { require(_allowances[msg.sender][_from] >= _value, "!Alw"); require((_balances[_from] >= _value) && (_balances[_from] != 0), "!Bal"); _balances[_from] -= _value; _balances[_to] += _value; _allowances[msg.sender][_from] -= _value; emit Transfer(_from, _to, _value); return true; } In the above code, we are checking if the transaction sender has enough permissible allowance to send tokens from another wallet, and we perform the same tasks as transfer() function, but the wallet chosen to send from will be affected by balance changes and not the transaction sender. We also reduce the permissible allowance by the number of tokens sent in this transaction. This way, nobody can spam the allowance logic and get all the tokens. Creating the approve() function Let's create a function that allows the token holder to set allowances for other wallets to transfer their tokens. function approve(address _spender, uint256 _value) public returns(bool) { require(_balances[msg.sender] >= _value, "!Bal"); _allowances[_spender][msg.sender] = _value; emit Approval(msg.sender, _spender, _value); return true; } In the above code, we check if the token holder has enough balance for the required allowance and revert if not. Then we are setting the allowance by making changes to the _allowances mapping. Finally, we are emitting an Approval() event so that apps can listen to this event. Creating the allowance() function Let's create a function that lets the token holder access their remaining allowance. function allowance(address _owner, address _spender) public view returns(uint256) { return _allowances[_spender][_owner]; } The above function is pretty straightforward. We access the _allowance mapping and return a spender's allowance for a token holder. Final code Here's the full code for the ERC-20 contract. // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; contract ERC20 { string public name; string public symbol; uint8 public immutable decimals; uint256 public immutable totalSupply; mapping(address => uint256) _balances; mapping(address => mapping(address => uint256)) _allowances; event Transfer(address indexed _from, address indexed _to, uint256 _value); event Approval(address indexed _owner, address indexed _spender, uint256 _value); constructor(string memory _name, string memory _symbol, uint256 _totalSupply) { name = _name; symbol = _symbol; decimals = 18; totalSupply = _totalSupply; _balances[msg.sender] = _totalSupply; } function balanceOf(address _owner) public view returns(uint256) { require(_owner != address(0), "!ZA"); return _balances[_owner]; } function transfer(address _to, uint256 _value) public returns(bool) { require((_balances[msg.sender] >= _value) && (_balances[msg.sender] != 0), "!Bal"); _balances[msg.sender] -= _value; _balances[_to] += _value; emit Transfer(msg.sender, _to, _value); return true; } function transferFrom(address _from, address _to, uint256 _value) public returns(bool) { require(_allowances[msg.sender][_from] >= _value, "!Alw"); require((_balances[_from] >= _value) && (_balances[_from] != 0), "!Bal"); _balances[_from] -= _value; _balances[_to] += _value; _allowances[msg.sender][_from] -= _value; emit Transfer(_from, _to, _value); return true; } function approve(address _spender, uint256 _value) public returns(bool) { require(_balances[msg.sender] >= _value, "!Bal"); _allowances[_spender][msg.sender] = _value; emit Approval(msg.sender, _spender, _value); return true; } function allowance(address _owner, address _spender) public view returns(uint256) { return _allowances[_spender][_owner]; } } Deploying and testing the contract Typically to deploy and test the contract functions, you'll need to write scripts and add private keys to your environment variables to deploy and interact with the contract. However, thirdweb deploy provides an easy and interactive workflow for deploying your contracts on the blockchain without exposing your private keys - and it's FREE! You don't need any additional setup- run the following command in the terminal to begin deploying the contract. npx thirdweb@latest deploy This command will do all the hard work- compiling the contract and uploading the data to IPFS, and then you will be redirected to your browser to pass in the parameters to deploy the contract finally. Also, as you can see, thirdweb detects that this is indeed an ERC-20 token as it follows the standards that need to be satisfied for a contract to be classified as an ERC-20 token. You can now enter the information in the parameters. Note that in _totalSupply you need to add 18 zeros after your total supply amount because we use 18 decimals in this token contract. You can now choose your desired chain and click on Deploy Now. This should initiate a transaction request on your chosen provider (MetaMask, WalletConnect, etc.). After completing the deployment, you should be redirected to the contract dashboard. Choose Tokens from the top menu, and you should see the following interface. You can see all the data about your contract- the total supply, decimals, tokens owned by the connected wallet, airdrop and transfer tokens functionality. You can also use the Explorer tab to interact with each of the functions without writing a script to do so. Watch my video on writing ERC-20 tokens from scratch to know more and test your contract. There we test this token using the contract explorer. Conclusion In this article, we saw how to write an ERC-20 token from scratch and deploy it using thirdweb. I hope this article explained how these tokens work under the hood and how every line is very important. If you have any questions or feedback, feel free to comment below or reach out to me on other platforms.

Testing dApps on Ethereum Mainnet

Testing dApps on Ethereum Mainnet

Atharva Deosthale

This article will show how you can fork the Ethereum mainnet to simulate contract calls for testing without using real ETH.