Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update code in /sample-dapps/evm-token-factory To Support Foundry #67

Merged
merged 6 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 69 additions & 33 deletions sample-dapps/evm-token-factory/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

# QuickNode EVM Token Factory Demo

## Overview
Expand All @@ -12,10 +13,22 @@ The demo uses [Next.js 14](https://nextjs.org/) project bootstrapped with [`crea

## Getting Started

Open the project directory:

```bash
cd sample-dapps/evm-token-factory
```

### Set Environment Variables

1. Rename `.env.example` to `.env.local `and update it with RPC URLs for each blockchain. Also, include your [WalletConnect](https://cloud.walletconnect.com/). project ID (optionally, you can leave this blank but some features will not be supported). To create RPC URLs for each chain, you can run your own node locally or use a service like [QuickNode](https://quicknode.com) to quickly spin up a node endpoint.

### Configure Smart Contract Addresses

2. Update the `factoryAddress` value in `evm-token-factory/app/utils/ethereum.ts` with your deployed factory contract address. This is the address you received in the output during the [Deployment](https://quicknode.com/guides/ethereum-development/dapps/how-to-create-an-evm-token-factory-dapp#deployment) section.

3. Remove any unused chains (e.g., mainnet) from the `src/context/web3modal.tsx` file.

### RPC Configuration

This app requires a valid RPC URL for each blockchain you want to support. Here are more details on how RPC is configured throughout the app.
Expand All @@ -27,12 +40,6 @@ If you do not want to support a blockchain(s), you can remove references of the

### Install Dependencies

Open the project directory:

```bash
cd sample-dapps/evm-token-factory
```

Then, install the dependencies:

```bash
Expand All @@ -45,7 +52,7 @@ pnpm install
bun install
```

First, run the development server:
After, start the development server:

```bash
npm run dev
ferhatqn marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -64,30 +71,59 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the
1. Connect your wallet
- Make sure you have enough ETH (or other native EVM gas token) in your wallet to cover the create token transaction
- If you are using Testnet, you can get free ETH from the [QuickNode Faucet](https://faucet.quicknode.com/)
2. Click "Create Token" and confirm the transaction to create the ERC-2O token!
2. Click "Create Token" and confirm the transaction to create the ERC-20 token!

### Architecture

```bash
src/
├── app/
│ ├── page.tsx # Main page for Token Factory
│ └── layout.tsx # Import the Web3Modal component
│ └── api/
│ └── evm/
│ └── createToken/route.ts # Create New ERC-20 Transaction
└── components/
| ├── Connect.tsx # Web3Modal Component
| ├── Navbar.tsx # Navbar component
└── Footer.tsx # Footer Component
└── context/
├── web3modal.tsx # Wallet Adapter Context providers
└── smart_contracts/
│ └── abi/
│ └── factory.json # Factory ABI
│ └── Factory.sol # Token Factory
│ └── Token.sol # Token Details
├── .env.local # Configure RPCs and WalletConnect Project ID

```sh
├── app
│   ├── api
│   │   └── evm
│   │   └── createToken
│   │   └── route.ts # API Method for calling CreateToken function
│   ├── components
│   │   ├── Connect.tsx # Web3Modal Component
│   │   ├── Footer.tsx
│   │   └── Navbar.tsx
│   ├── favicon.ico
│   ├── globals.css
│   ├── layout.tsx # The Web3Modal component
│   ├── page.tsx # Main page for Token Factory
│   └── utils
│   ├── abi.json # Factory ABI
│   └── ethereum.ts # Chain configuration and helpers
├── context
│   └── web3modal.tsx # Wallet Adapter Context providers
├── contracts
│   ├── README.md
│   ├── foundry.toml # Forge configuration
│   ├── lib # Dependencies
│   ├── remappings.txt # Library mappings
│   ├── script
│   │   ├── Counter.s.sol
│   │   └── CreateToken.s.sol
│   ├── src
│   │   ├── Counter.sol
│   │   ├── Factory.sol
│   │   └── Token.sol
│   └── test
│   ├── Counter.t.sol
│   ├── Factory.t.sol
│   └── Token.t.sol
├── next-env.d.ts
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── public
│   ├── next.svg
│   ├── preview.png
│   ├── preview2.png
│   └── vercel.svg
├── .env.example # Configure RPCs and WalletConnect Project ID
├── tailwind.config.ts
└── tsconfig.json
```

## Smart Contracts
Expand All @@ -96,9 +132,9 @@ The ERC-20 Token Factory backend built on smart contracts with Solidity can be d

The ERC-20 Token Factory is built with two smart contracts:

- **Factory**: The Factory contract (`smart-contracts/Factory.sol`) inherits the Token.sol smart contract and acts as a Factory for creating and tracking new ERC-20 tokens.
- **Factory**: The Factory contract (`contracts/Factory.sol`) inherits the Token.sol smart contract and acts as a Factory for creating and tracking new ERC-20 tokens.

- **Token**: This is an ERC-20 smart contract (`smart-contracts/Token.sol`) defined by the OpenZeppelin standard and includes a `mint` and `transferOwnership` function call in the constructor upon deployment.
- **Token**: This is an ERC-20 smart contract (`contracts/Token.sol`) defined by the OpenZeppelin standard and includes a `mint` and `transferOwnership` function call in the constructor upon deployment.

### Supported Chains & Addresses

Expand All @@ -109,11 +145,11 @@ The ERC-20 Token Factory is built with two smart contracts:

To deploy the Factory contract on a new chain using Foundry, follow these steps:

1. Ensure [Foundry](https://book.getfoundry.sh/) is installed and navigate inside the `smart-contracts` directory. Install the required dependencies with the following commands:
1. Ensure [Foundry](https://book.getfoundry.sh/) is installed and navigate inside the `contracts` directory. Install the required dependencies with the following commands:

```sh
forge install OpenZeppelin/openzeppelin-contracts --no-commit
forge install foundry-rs/forge-std --no-commit
forge install OpenZeppelin/openzeppelin-contracts --no-commit
```

2. Build (compile) the smart contracts using the `forge build` command.
Expand All @@ -125,7 +161,7 @@ forge install foundry-rs/forge-std --no-commit
```sh
forge create --rpc-url QUICKNODE_HTTP_URL \
--private-key YOUR_PRIVATE_KEY \
src/Factory.sol:Factory
src/Factory.sol:TokenFactory
```

5. Edit the `src/context/web3modal.tsx` file and add a new chain object with its chain ID (find a list [here](https://chainlist.org/)), name, native gas token currency, explorer URL, and RPC URL (e.g., QuickNode):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ethers } from 'ethers';
import { RequestBody, CHAINS } from '@/app/utils/ethereum';
import abi from '@/smart-contracts/abi/factory.json'
import abi from '@/app/utils/abi.json';

export async function POST(request: Request) {
try {
Expand Down
1 change: 1 addition & 0 deletions sample-dapps/evm-token-factory/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export default function Home() {
const chainConfig = CHAINS[chainId ?? 1];
const populateTxn = await signer.populateTransaction(data.apiResponse)
const send = await signer.sendTransaction(populateTxn)
await send.wait()
const receipt = await ethersProvider.getTransactionReceipt(send.hash)
const tokenAddressLogs = receipt?.logs[3].topics[1] as string;
const tokenAddress = decoder.decode(['address'], tokenAddressLogs)[0]
Expand Down
2 changes: 1 addition & 1 deletion sample-dapps/evm-token-factory/app/utils/abi.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
[{"inputs":[{"internalType":"address","name":"initialOwner","type":"address"},{"internalType":"uint256","name":"initialSupply","type":"uint256"},{"internalType":"string","name":"name","type":"string"},{"internalType":"string","name":"symbol","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"allowance","type":"uint256"},{"internalType":"uint256","name":"needed","type":"uint256"}],"name":"ERC20InsufficientAllowance","type":"error"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"uint256","name":"balance","type":"uint256"},{"internalType":"uint256","name":"needed","type":"uint256"}],"name":"ERC20InsufficientBalance","type":"error"},{"inputs":[{"internalType":"address","name":"approver","type":"address"}],"name":"ERC20InvalidApprover","type":"error"},{"inputs":[{"internalType":"address","name":"receiver","type":"address"}],"name":"ERC20InvalidReceiver","type":"error"},{"inputs":[{"internalType":"address","name":"sender","type":"address"}],"name":"ERC20InvalidSender","type":"error"},{"inputs":[{"internalType":"address","name":"spender","type":"address"}],"name":"ERC20InvalidSpender","type":"error"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"OwnableInvalidOwner","type":"error"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"OwnableUnauthorizedAccount","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"}]
[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"tokenAddress","type":"address"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"uint256","name":"initialSupply","type":"uint256"}],"name":"TokenCreated","type":"event"},{"inputs":[{"internalType":"address","name":"initialOwner","type":"address"},{"internalType":"uint256","name":"initialSupply","type":"uint256"},{"internalType":"string","name":"name","type":"string"},{"internalType":"string","name":"symbol","type":"string"}],"name":"createToken","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"nonpayable","type":"function"}]
14 changes: 14 additions & 0 deletions sample-dapps/evm-token-factory/contracts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Compiler files
cache/
out/

# Ignores development broadcast logs
!/broadcast
/broadcast/*/31337/
/broadcast/**/dry-run/

# Docs
docs/

# Dotenv file
.env
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@
src = "src"
out = "out"
libs = ["lib"]
remappings = ["@openzeppelin/=node_modules/@openzeppelin/openzeppelin-contracts/"]

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
1 change: 1 addition & 0 deletions sample-dapps/evm-token-factory/contracts/remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
12 changes: 12 additions & 0 deletions sample-dapps/evm-token-factory/contracts/script/Counter.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script, console} from "forge-std/Script.sol";

contract CounterScript is Script {
function setUp() public {}

function run() public {
vm.broadcast();
}
}
23 changes: 23 additions & 0 deletions sample-dapps/evm-token-factory/contracts/script/CreateToken.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Script.sol";
import "../src/Factory.sol";

contract CreateToken is Script {
function run() public {
vm.startBroadcast();

address initialOwner = msg.sender;
uint256 initialSupply = 1000;
string memory name = "MyToken";
string memory symbol = "MTK";

TokenFactory factory = new TokenFactory();

address tokenAddress = factory.createToken(initialOwner, initialSupply, name, symbol);
console.log("Token created at address:", tokenAddress);

vm.stopBroadcast();
}
}
14 changes: 14 additions & 0 deletions sample-dapps/evm-token-factory/contracts/src/Counter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract Counter {
uint256 public number;

function setNumber(uint256 newNumber) public {
number = newNumber;
}

function increment() public {
number++;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ contract TokenFactory {
return address(newToken);

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import "openzeppelin-contracts/contracts/access/Ownable.sol";

contract Token is ERC20, Ownable {
constructor(address initialOwner, uint256 initialSupply, string memory name, string memory symbol)
constructor(address initialOwner, uint256 initialSupply, string memory
name, string memory symbol)
ERC20(name, symbol)
Ownable(initialOwner)
{
_transferOwnership(initialOwner);
_mint(initialOwner, initialSupply * 10 ** 18);

}
}
}
24 changes: 24 additions & 0 deletions sample-dapps/evm-token-factory/contracts/test/Counter.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test, console} from "forge-std/Test.sol";
import {Counter} from "../src/Counter.sol";

contract CounterTest is Test {
Counter public counter;

function setUp() public {
counter = new Counter();
counter.setNumber(0);
}

function test_Increment() public {
counter.increment();
assertEq(counter.number(), 1);
}

function testFuzz_SetNumber(uint256 x) public {
counter.setNumber(x);
assertEq(counter.number(), x);
}
}
35 changes: 35 additions & 0 deletions sample-dapps/evm-token-factory/contracts/test/Factory.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import "../src/Factory.sol";
import "../src/Token.sol";

contract TokenFactoryTest is Test {
TokenFactory factory;
address initialOwner;
uint256 initialSupply = 1000;
string name = "MyToken";
string symbol = "MTK";

event TokenCreated(address indexed tokenAddress, address indexed owner, uint256 initialSupply);

function setUp() public {
factory = new TokenFactory();
initialOwner = address(this);
}

function testCreateToken() public {
vm.expectEmit(false, false, false, false);
emit TokenCreated(address(0), initialOwner, initialSupply);

address tokenAddr = factory.createToken(initialOwner, initialSupply, name, symbol);
assertTrue(tokenAddr != address(0), "Token creation failed");

Token token = Token(tokenAddr);
assertEq(token.owner(), initialOwner, "Owner is not set correctly");
assertEq(token.totalSupply(), initialSupply * 10 ** 18, "Initial supply is incorrect");
assertEq(token.name(), name, "Token name is incorrect");
assertEq(token.symbol(), symbol, "Token symbol is incorrect");
}
}
33 changes: 33 additions & 0 deletions sample-dapps/evm-token-factory/contracts/test/Token.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "forge-std/Test.sol";
import "../src/Token.sol";

contract TokenTest is Test {
Token token;
address initialOwner;
uint initialSupply = 1000;
string name = "TestToken";
string symbol = "TT";

function setUp() public {
initialOwner = address(this);
token = new Token(initialOwner, initialSupply, name, symbol);
}

function testInitialOwner() public view {
assertEq(token.owner(), initialOwner);
}

function testInitialSupply() public view {
uint expectedSupply = initialSupply * 10 ** token.decimals();
assertEq(token.totalSupply(), expectedSupply);
assertEq(token.balanceOf(initialOwner), expectedSupply);
}

function testNameAndSymbol() public view {
assertEq(token.name(), name);
assertEq(token.symbol(), symbol);
}
}
Loading
Loading