How to deploy new instances using beacon proxy from a factory when using @openzeppelin/hardhat-upgrades

I have a logic contract which I have been able to deploy using @openzeppelin/hardhat-upgrades by following the instructions in the docs

// scripts/create-box.js
const { ethers, upgrades } = require("hardhat");

async function main() {
  const Box = await ethers.getContractFactory("Box");

  const beacon = await upgrades.deployBeacon(Box);
  await beacon.deployed();
  console.log("Beacon deployed to:", beacon.address);

  const box = await upgrades.deployBeaconProxy(beacon, Box, [42]);
  await box.deployed();
  console.log("Box deployed to:", box.address);
}

I'm trying to deploy new instances of my logic using a factory, but i'm not so sure how to do that. My assumption is that the package creates and deploys a beacon proxy for you, but how does one use that to create a new instance of the logic in a factory.

:1234: Code to reproduce

// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.6;

import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
import "./BoxBeacon.sol";
import "./BoxV1.sol";

contract BoxFactory {
    mapping(uint32 => address) private boxes;

    BeaconProxy public immutable beacon;

    constructor(address _beacon, bytes memory data) {
        beacon = new BeaconProxy(_beacon, data);
    }

    function buildBox(
        uint256 _age,
        string memory _name,
        uint32 _boxId
    ) public {
        BeaconProxy box = new BeaconProxy(
            address(beacon),
            abi.encodeWithSelector(
                BoxV1(address(0)).initialize.selector,
                _age,
                _name
            )
        );
        boxes[_boxId] = address(box);
    }

    function getBeacon() public view returns (address) {
        return address(beacon);
    }

    function getBoxByIndex(uint32 index) public view returns (address) {
        return boxes[index];
    }
}

:computer: Environment

Environment is hardhat

  • "@openzeppelin/hardhat-upgrades": "^1.17.0"

Hi @gate3,

The deployBeacon function deploys up to 2 contracts: an implementation (if it has not already been deployed) and a beacon.

The deployBeaconProxy function does not deploy the implementation or beacon. It only deploys 1 contract: a beacon proxy which points to an existing beacon.

If you are trying to deploy new beacon proxies using a factory, then you would be using your factory instead of deployBeaconProxy. In your example, your immutable beacon variable should not be a BeaconProxy, but it should be an UpgradeableBeacon (which can be the beacon address that was deployed by deployBeacon).

See this post for another example of a beacon proxy factory.

1 Like

Thank you so much @ericglau, this helps a lot. I have another question though, so I watched your talk at the community meeting where you mentioned that we can create as many proxies as needed using deployBeaconProxy. I tested it out and i can see multiple instances are created which also achieves my aim. Is this the right way to do it or would it be better creating a factory like in the example. Also if this is the right way, do I still need to take care of things like transferring ownership or is that handled already if use my address as the deployer when deploying with the hardhat plugin. Apologies if my questions are too Noob, still getting used to the pattern.

The factory would provide an on-chain way of creating new instances, whereas deployBeaconProxy would need to be called through a script. It depends on your use case or how you want to deploy the new beacon proxies.

In terms of ownership, you are correct that your address that you used to deploy the beacon using the hardhat plugin would be the owner by default. You can see that UpgradeableBeacon inherits Ownable and its upgradeTo function uses the onlyOwner modifier. So, only the owner can upgrade the beacon, and the owner is whoever created the contract.

1 Like

Thanks a lot @ericglau .. For anybody interested in how i went about this eventually. I used an hybrid solution.. I deployed a beacon and implementation using deployBeacon, but i wanted an on-chain solution for the factory. So my factory ended up looking like this

// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.6;

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

contract BoxFactory {
    address private immutable boxBeacon;
    mapping(uint32 => address) private boxes;

    event BoxDeployed(address tokenAddress);

    constructor(address _boxBeacon) {
        boxBeacon = address(_boxBeacon);
    }

    function buildBox(
        uint256 _age,
        string calldata _name,
        uint32 _boxId
    ) external returns (address) {
        BeaconProxy proxy = new BeaconProxy(
            boxBeacon,
            abi.encodeWithSelector(BoxV1.initialize.selector, _age, _name)
        );
        boxes[_boxId] = address(proxy);
        emit BoxDeployed(address(proxy));
        return address(proxy);
    }

    function getBoxByIndex(uint32 index) external view returns (address) {
        return boxes[index];
    }
}

I'm taking the beacon from using deployBeacon and passing its address to the factory.

const BoxFactory = await ethers.getContractFactory(
      "BoxFactory",
      contractOwner
);

bfactory = await BoxFactory.deploy(beacon.address);
await bfactory.deployed();
console.log("factory deployed to:", bfactory.address);

await (await bfactory.buildBox(21, "SomeName", 1)).wait();
1 Like

Thanks for the example here @gate3 as I'm also doing something similar.
I haven't done a ton of testing yet, but I'm thinking the

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

in your factory contract is unnecessary, correct? Since "upgrades.deployBeacon" creates an UpgradeableBeacon for us? I was able to deploy everything fine without that import in my factory contract, but want to double check that it's not indeed necessary for some reason @ericglau ?

Right, that import doesn't look necessary.

1 Like

Thanks for pointing this out @scoots .. I should have updated the code showing where I removed it. Will do that, thanks again.

1 Like