Skip to content

Ectario/NFTFactory

Repository files navigation

NFT Factory - Deployment and Upgrade Guide

This repository contains an upgradeable ERC721 NFT contract using Foundry and OpenZeppelin's UUPS Proxy.

Prerequisites

Ensure you have the following installed:

  • Foundry (Installation Guide)
  • Anvil (included with Foundry)
  • A funded wallet if deploying on a testnet or mainnet

Setting Up a Local Environment

Running a Local Blockchain with Anvil

Anvil provides a local Ethereum blockchain for testing. Start it with:

anvil

By default, it runs on http://127.0.0.1:8545 and pre-funds test accounts with 10,000 ETH.

Setting Environment Variables

To simplify deployment, set up environment variables:

export PRIVATE_KEY=0xYourPrivateKeyHere # Or the private key given by anvil if this is a local testing
export RPC_URL=http://127.0.0.1:8545

Alternatively, create a .env file and load it:

source .env

Running Tests

Run all tests:

forge test

Run a specific test:

forge test --match-test testFunctionName

Enable detailed traces:

forge test -vvv

Deploying Locally

Deploy the Contract

The deployment script initializes both the contract implementation and the proxy:

export PRIVATE_KEY=0xYourPrivateKeyHere
forge script scripts/Deploy.s.sol --broadcast --rpc-url $RPC_URL

This script will:

  • Deploy the implementation contract (TokenFactoryImplem.sol).
  • Deploy a UUPS Proxy that points to the implementation.
  • Call the initialize() function on the implementation through the proxy.

Using the Proxy

After deployment you can store the proxy address and for further interaction you can do the following:

export PROXY_ADDRESS=0xLoggedAddressAfterDeployment

Verify the contract is working:

cast call $PROXY_ADDRESS "getDistributorName()" --rpc-url $RPC_URL

Upgrading the Contract

How to Modify the Contract

If you want to upgrade the contract, modify the logic in _contracts/TokenFactoryImplem.sol.
Ensure you do not change the contract's storage layout.
For more details on upgradeable contracts and storage compatibility, see OpenZeppelin's guide:
https://docs.openzeppelin.com/upgrades-plugins/1.x/proxies#upgrading

Running the Upgrade

Deploy the new implementation and upgrade the proxy to point to it:

export PROXY_ADDRESS=0xLoggedAddressAfterDeployment
export PRIVATE_KEY=0xYourPrivateKeyHere
forge script scripts/Upgrade.s.sol --broadcast --rpc-url $RPC_URL

This will:

  • Deploy the updated contract (TokenFactoryImplem.sol). NOTE: You can directly modify the TokenFactoryImplem in _contracts/ and then run the script to upgrade it
  • Upgrade the proxy to use the new implementation.

Verify the Upgrade

After performing an upgrade, you can confirm that the new implementation is active by checking the contract version. The contract includes a version() function that increments automatically with each upgrade.

cast call $PROXY_ADDRESS "version()" --rpc-url $RPC_URL

Additionally, you can verify that the new logic is in use by calling a function that was modified in the new implementation, this is an example with getDistributorName:

cast call $PROXY_ADDRESS "getDistributorName()" --rpc-url $RPC_URL

If the upgrade was successful, version() should return an incremented value, and getDistributorName() should reflect any changes made in the upgraded contract.

Deploying on Mainnet

Using a Free Public RPC Endpoint

Instead of running Anvil, you can deploy to Polygon mainnet using a public RPC:

export RPC_URL=https://polygon-rpc.com

Deployment on Mainnet

forge script scripts/Deploy.s.sol --broadcast --rpc-url $RPC_URL

Upgrading on Mainnet

after some changes in _contracts/TokenFactoryImplem.sol

forge script scripts/Upgrade.s.sol --broadcast --rpc-url $RPC_URL

Minting an NFT

To mint a new NFT, call the mintNFT function from the contract using the proxy address. This function requires the recipient’s address and the metadata URI of the token:

cast send $PROXY_ADDRESS "mintNFT(address,string)" 0xRecipientAddress "ipfs://your-metadata-uri" --rpc-url $RPC_URL --private-key $PRIVATE_KEY

Only the contract owner can execute this function. After minting, you can verify the token's metadata with:

cast call $PROXY_ADDRESS "tokenURI(uint256)" 0 --rpc-url $RPC_URL

Replace 0 with the correct token ID if multiple NFTs exist.

Retrieving NFTs Owned by an Address

To check which NFTs are owned by a specific address, use the tokensOfOwner function. This function returns an array of token IDs belonging to the specified address.

Retrieve All NFTs of an Address

cast call $PROXY_ADDRESS "tokensOfOwner(address)" 0xUserAddress --rpc-url $RPC_URL

This will return an array of token IDs owned by 0xUserAddress. If the user owns multiple NFTs, you will get something like:

[1, 3, 7, 10]

If the array is empty ([]), the user does not own any NFTs.

Check Metadata of a Specific NFT

Once you have the token ID, you can check its metadata using:

cast call $PROXY_ADDRESS "tokenURI(uint256)" 1 --rpc-url $RPC_URL

Replace 1 with the actual token ID from the previous step.

Check Ownership of a Specific NFT

To verify the owner of a specific NFT:

cast call $PROXY_ADDRESS "ownerOf(uint256)" 1 --rpc-url $RPC_URL

This will return the wallet address that owns token ID 1.
These commands allow you to track which tokens belong to a specific address and verify their metadata.

Commands Overview

Task Command
Start local blockchain anvil
Run tests forge test
Deploy contract locally export RPC_URL=<RPC_URL> && export PRIVATE_KEY=0xYourPrivateKeyHere && forge script scripts/Deploy.s.sol --broadcast --rpc-url $RPC_URL
Upgrade contract export RPC_URL=<RPC_URL> && export PROXY_ADDRESS=0xLoggedAddressAfterDeployment && export PRIVATE_KEY=0xYourPrivateKeyHere && forge script scripts/Upgrade.s.sol --broadcast --rpc-url $RPC_URL
Check implementation version cast call $PROXY_ADDRESS "version()" --rpc-url $RPC_URL
Mint an NFT cast send $PROXY_ADDRESS "mintNFT(address,string)" 0xRecipientAddress "ipfs://your-metadata-uri" --rpc-url $RPC_URL --private-key $PRIVATE_KEY
Check token metadata cast call $PROXY_ADDRESS "tokenURI(uint256)" <TokenID> --rpc-url $RPC_URL
Check all NFTs owned by an address cast call $PROXY_ADDRESS "tokensOfOwner(address)" 0xUserAddress --rpc-url $RPC_URL
Check the owner of a specific NFT cast call $PROXY_ADDRESS "ownerOf(uint256)" <TokenID> --rpc-url $RPC_URL
Burn an NFT cast send $PROXY_ADDRESS "burn(uint256)" <TokenID> --rpc-url $RPC_URL --private-key $PRIVATE_KEY

Security aspect

Although some testing has been conducted, it's important to note that this contract is relatively small and straightforward, leveraging OpenZeppelin's battle-tested libraries for ERC721 functionality and upgradeability. Given that most of the core logic (minting, burning, ownership tracking) is inherited from OpenZeppelin's well-audited contracts, the likelihood of vulnerabilities is minimal.

However, the tests includes static analysis, fuzzing, and unit tests. It has been performed to ensure robustness and correctness, even if it may seem overkill for such a simple implementation.

Static analysis

No problematic pattern found.

Slither: slither . --json slither_out.json --filter-paths "lib/*"

. analyzed (30 contracts with 94 detectors), 0 result(s) found

{
    "success": true,
    "error": null,
    "results": {}
}

Fuzzing & Testing

No problematic behavior found.

Foundry: forge test --fuzz-runs 1000

Ran 12 tests for tests/TokenFactoryImplemTest.t.sol:TokenFactoryImplemTest

[PASS] testBurnByNonOwner() (gas: 149784)
[PASS] testBurnByNonOwnerDoesNotAffectTrueOwner() (gas: 156876)
[PASS] testBurnNFT() (gas: 120035)
[PASS] testBurnNonExistentToken() (gas: 15647)
[PASS] testInitializeCanOnlyBeCalledOnce() (gas: 15624)
[PASS] testMintFailByNonOwner() (gas: 18672)
[PASS] testMintNFT() (gas: 149116)
[PASS] testOwner() (gas: 17741)
[PASS] testTokensOfOwnerAfterBurn() (gas: 118983)
[PASS] testTokensOfOwnerAfterMint() (gas: 143745)
[PASS] testUpgrade() (gas: 1603011)
[PASS] testUpgradeFail() (gas: 1593014)
Suite result: ok. 12 passed; 0 failed; 0 skipped; finished in 1.79ms (3.35ms CPU time)

Ran 1 test for tests/TokenFactoryImplemFuzz.t.sol:TokenFactoryImplemFuzz

[PASS] testMassMintBurn(uint256,uint256) (runs: 1001, μ: 93898751, ~: 88014325)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 74.87s (74.87s CPU time)

Ran 2 test suites in 74.87s (74.87s CPU time): 13 tests passed, 0 failed, 0 skipped (13 total tests)

About

Just a lil' upgradeable NFT Factory

Topics

Resources

License

Stars

Watchers

Forks