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.
2. Creating a clone
The TokenFactory creates a lightweight BeaconProxy — no logic code duplicated.
3. Interacting with the NFT
Users call the BeaconProxy. It delegates everything to Logic V1.
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.
5. Using the new logic
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.