Making an Upgradable MultiSig Contract


What I have is a typical Multisig Contract, which I have updated to be upgradable:

function initialize(
        address[] memory _owners,
        uint256 _numConfirmationsRequired
    ) public initializer {
        if (_owners.length == 0) revert OwnersRequired();

        if (
            _numConfirmationsRequired == 0 ||
            _numConfirmationsRequired >= _owners.length
        ) revert InvalidRequiredConfirmations();

        for (uint256 i = 0; i < _owners.length; i++) {
            address owner = _owners[i];

            if (owner == address(0)) revert InvalidOwner();

            if (isOwner[owner]) revert OwnerNotUnique();

            isOwner[owner] = true;

        numConfirmationsRequired = _numConfirmationsRequired;

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {

I am currently writing the tests for it, but I am facing trouble at the stage where I submit the upgrade as a transaction to the multisig to be confirmed and executed.

const implementationAddress = (await upgrades.prepareUpgrade(
      )) as string;

const adminInstance = await upgrades.admin.getInstance();

This stage is where it gets fuzzy for me. I am supposed to execute the equivalent of const data = await adminInstance.contract.methods.upgrade(implementationAddress).encodeABI();, but I am not sure how to implement it in a non-Truffle environment.

Namely: How do I encode the upgrade method, so as to use it as data for a transaction?

P.S. I have come across the following thread: How to transfer proxy admin ownership to a custom multisig wallet contract and upgrade proxy via a multisig? - #3 by Sol_Man
However, the solution is not clear to me, especially since the other person is using Truffle while I am on Hardhat.

In Hardhat, you can use interface.encodeFunctionData from the ethers contract factory to encode a function call.

For example, YourProxyAdminContractFactory.interface.encodeFunctionData('upgrade', [ proxyAddress, newImplementationAddress ]);