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

I tried the same solution for my use case. But I am stuck in ownership of the proxy. In the case of the script, the owner is the sender, but in the case of the factory, the owner of the new proxy is the contract. I am still finding a way to transfer the ownership, the transferOwnership of BeaconProxy is not accessible to my factory.

@gawshinde There are two different contexts for ownership: the owner of your contract is set in the BeaconProxy, but the owner of the beacon (who has permission to upgrade the beacon) is set in UpgradeableBeacon.

This example Deploying Upgradeable Proxies and Proxy Admin from Factory contract - #12 by frangio sets the beacon owner in the constructor, and sets the contract owner in createToken (passing the owner parameter to the initializer in the example implementation here).

Just posting this here for anybody that needs a solution in future. Here is a version of my implementation, still needs some update, but it contains testing and all of the basic things you will need to get started. I will update the README and put things in a better shape once I can find the time. Since its open source contributions are also welcome

Note do your own security checks and maybe add more tests as you see fit.

[https://github.com/weedle-app/weedle-NFT-ERC1155-Upgradeable-Beacon-Proxy](Git hub repo for my beacon proxy https://github.com/weedle-app/weedle-NFT-ERC1155-Upgradeable-Beacon-Proxy)

1 Like

My use case is to create contracts for my daap users. When I am using a factory, the creator is the factory, not the sender. As my contract is Role-based, I am finding a way to make my user an owner, as the open sea will either take creator or ownable as owner. Hope I am able to explain.

One way is to call the upgrade plugin directly from my web3 app and have user sign it. I do not know if it is possible.

@gawshinde Can you just pass in the desired owner (or the msg.sender from the factory) to the initializer, as per the link in my previous post?

If you are using AccessControl, then you can use _grantRole in the initializer with the passed in sender.

@gawshinde Take a look at the repo I shared. I passed in the owner as suggested by @ericglau and I also wrote tests to prove it works.

You can see here i'm passing the user I want to set as the owner. You just need to change that to the user you want, in my own case I want the factory deployer to still be the owner, but in your case just change it.

You can see here I'm transferring ownership to the passed user. You can also see I am using roles as well.

1 Like

I am passing owner and fixed opensea issue by just providing a public function to get owner as it is not available in non-ownable contracts. But what I really want to do is this,

  1. when I create a beacon proxy with script and plugin, the creator of the contract in the network is the address which runs the script.
  2. But when I do it using a proxy factory (though I am able to solve the ownership issue), The creator address in the transaction would be the address of the factory and not the address of the caller.

Is there a way I can call the script using web3 and have my user sign the trasection?

@gawshinde Did you transfer ownership in the initialize function of the specific contract instance that was created and passed your desired owner to it ?

Yes. But transferring ownership does not change the creator of the contract. I do not want to only transfer ownership, but want to make sure that the creator address is also user's and not factory's (check etherscan for creator address). While using script it works that way but in beacon proxy factory it does not.

Hi @gawshinde, I believe this is by design and the logical way this should work. Think about it like this

  • When using script, you can specify which account is the factory and that account will be the creator.
  • When using an on-chain factory, you are having a smart contract create an instance for you, which makes that smart contract the original creator.

I believe if what you want is to change who the creator is then maybe stick to the script method.

Agreed. However, I wanted to understand from this forum how the script can achieve this? As I looked into the code, it is also using a factory. I will dig in more to understand how it works.

1 Like

Im keep getting same Err over and over again.
VM Exception while processing transaction: reverted with reason string 'Address: low-level delegate call failed'

I change deploy file and Factory.sol many time but its unsuccess.
Does anyone know what it could possibly be?