Implementing Cheap NFT Clones Using BeaconProxy and UpgradableProxy Patterns

2022-02-14

#backend#development
Implementing Cheap NFT Clones Using BeaconProxy and UpgradableProxy Patterns

You want to deploy 1,000 NFT contracts. At full deployment cost each, your gas bill is absurd. You're paying to deploy the same logic a thousand times over.

The fix: deploy the logic once, then deploy lightweight proxies that point to it. Each proxy costs a fraction of a full deployment. And if you need to upgrade the logic later, you change it in one place and every proxy gets the update automatically.

This is the BeaconProxy + UpgradableProxy pattern, and it changes the economics of NFT deployment entirely.

How It Works

BeaconProxy: A thin contract that delegates all calls to a logic contract. It doesn't contain the logic itself — it asks a beacon "where's the implementation?" and forwards the call there. Deploy a thousand of these for the gas cost of a few full contracts.

UpgradableBeacon: The single source of truth for the implementation address. Point it at a new logic contract, and every BeaconProxy in existence starts using the new code. One transaction to upgrade them all.

The NFTFactory

An NFTFactory smart contract ties it together. It deploys the beacon once, then produces cheap proxy clones on demand.

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
import "./TokenV1.sol";

contract NFTFactory {
    UpgradeableBeacon immutable beacon;

    event TokenCreated (
      address indexed cloneAddress
    );

    constructor(address implementationAddress) {
        beacon = new UpgradeableBeacon(implementationAddress);
        transferOwnership(msg.sender);
    }

    function upgradeImplementation(address newImplementation) public onlyOwner {
        beacon.upgradeTo(newImplementation);
    }

    function createNFT() public returns (address) {
        BeaconProxy token = new BeaconProxy(
            address(beacon),
    abi.encodeWithSelector(TokenV1(address(0)).initialize.selector)
        );

        emit TokenCreated(address(token));
        address(token);
    }
}

Upgrading the logic is one function call:

// Inside the NFTFactory contract
function setLogic(address _newLogic) public {
    UpgradeableBeacon(beacon).upgradeTo(_newLogic);
}

The UpgradableProxy lets you change what the beacon points to without changing any proxy's address:

// Inside the NFTFactory contract
function upgradeNFT(address _nftAddress, address _newLogic) public {
    BeaconProxy(_nftAddress).upgradeTo(_newLogic);
}

You only need the TokenFactory for this to work. OpenZeppelin provides both BeaconProxy and UpgradableBeacon out of the box.

// Inside the NFTFactory contract
function createAndUpgradeNFT(address _newLogic) public returns (address) {
    address nft = createNFT();
    upgradeNFT(nft, _newLogic);
    return nft;
}

The Flow, Visualized

1. Structure setup

Deploy the logic contract, the token factory, and the UpgradableBeacon.

Content image

2. Creating a clone

The TokenFactory creates a lightweight BeaconProxy — no logic code duplicated.

Content image

3. Interacting with the NFT

Users call the BeaconProxy. It delegates everything to Logic V1.

Content image

4. Upgrading the logic

Call upgradeTo(new address) on the UpgradableBeacon. From this point forward, every BeaconProxy fetches the new address and delegates to it.

Content image

5. Using the new logic

Content image

Deploy the logic once. Clone it cheaply. Upgrade it everywhere with a single transaction. That's how you scale NFT deployments without scaling your gas bill.