Failed to upgrade implementation of proxy using proxy admin

Here is my contract code:

// SPDX-License-Identifier: MIT
pragma solidity =0.8.27;
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {IOracle} from "./interfaces/IOracle.sol";
import {ISupraVerifier} from "./interfaces/ISupraVerifier.sol";
import {ILpProvider} from "./interfaces/ILpProvider.sol";
import {Crypto} from "./libs/Crypto.sol";
import {Dex} from "./libs/Dex.sol";
import {SupraOracleDecoder} from "./libs/SupraOracleDecoder.sol";

/**
 * @title Vault
 * @dev A contract for managing user funds and positions
 * This contract allows users to deposit and withdraw funds, open and close positions,
 * and manage their trades.
 * @custom:oz-upgrades-from Vault
 */
contract Vault is
    OwnableUpgradeable,
    ReentrancyGuardUpgradeable,
    PausableUpgradeable
{
    using SafeERC20 for IERC20;

    /**
     * @dev Public variable to store the signature expiry time.
     */
    uint256 public signatureExpiryTime;

    /**
     * @dev Private constant to store the SECP256K1 curve N value.
     */
    uint256 private constant SECP256K1_CURVE_N =
        0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141;

    /**
     * @dev Private variable to store the request ID counter.
     */
    uint32 private _requestIdCounter;

    /**
     * @dev Public mapping to store disputes based on their IDs.
     */
    mapping(uint32 => Dispute) public _disputes;

    // mapping(uint32 => ClosePositionDispute) private _positionDisputes;

    /**
     * @dev Private mapping to track used signatures.
     */
    mapping(bytes => bool) private _signatureUsed;

    /**
     * @dev Private mapping to track used Schnorr signatures.
     */
    mapping(bytes => bool) private _schnorrSignatureUsed;

    /**
     * @dev Private mapping to store the latest Schnorr signature ID.
     */
    mapping(uint32 => uint32) private _latestSchnorrSignatureId;

    /**
     * @dev Public mapping to store combined public keys.
     */
    mapping(address => address) public combinedPublicKey;

    /**
     * @dev Public mapping to check if a token is supported.
     */
    mapping(address => bool) public isTokenSupported;

    /**
        * @dev Constant representing the value 1e9.
        check if this is unused and in this case remove it
        */

    uint256 constant ONE = 1e9;

    /**
     * @dev constant representing the percentage precision value.
     */
    uint256 constant PRECISION_PERCENTAGE = 10_000;

    /**
     * @dev Public variable to store the LP provider address.
     */
    address public lpProvider;

    /**
     * @dev Public variable to store the DEX supporter address.
     */
    address public dexSupporter;

    uint256 public lastPausedTime;

    /**
     * @dev Struct to represent token balances.
     */

    struct TokenBalance {
        address token;
        uint256 balance;
    }

    event Deposited(
        address indexed user,
        address indexed token,
        uint256 amount
    );
    event Withdrawn(
        address indexed user,
        address indexed token,
        uint256 amount
    );
    event WithdrawalRequested(
        address indexed user,
        address indexed token,
        uint256 amount,
        uint32 requestId
    );
    event TokenAdded(address indexed token);
    event TokenRemoved(address indexed token);
    event LPProvided(
        address indexed user,
        address indexed token,
        uint256 amount
    );
    event LPWithdrawn(
        address indexed user,
        address indexed token,
        uint256 amount
    );
    event PartialLiquidation(
        address indexed user,
        address indexed token,
        uint256 amount
    );

    error InvalidSignature();
    error InvalidUsedSignature();
    error InvalidSchnorrSignature();
    error InvalidSP();
    error ECRecoverFailed();
    error InvalidAddress();
    error DisputeChallengeFailed();
    error SettleDisputeFailed();
    error DataNotVerified();

    struct WithdrawParams {
        address trader;
        address token;
        uint256 amount;
        uint64 timestamp;
    }

    // for liquidation case
    struct OraclePrice {
        string positionId;
        address token;
        uint256 price;
        uint64 timestamp;
    }

    enum DisputeStatus {
        None,
        Opened,
        Challenged,
        Settled
    }

    struct Dispute {
        address user;
        address challenger;
        uint64 timestamp;
        Crypto.Balance[] balances;
        Crypto.Position[] positions;
        uint8 status;
        uint32 sessionId;
    }

    struct ClosePositionDispute {
        address user;
        address challenger;
        uint64 timestamp;
        Crypto.Position[] positions;
        uint8 status;
        uint32 sessionId;
    }
    event DisputeOpened(uint32 requestId, address indexed user);
    event DisputeChallenged(uint32 requestId, address indexed user);
    event PositionDisputeChallenged(uint32 requestId, address indexed user);
    event DisputeSettled(uint32 requestId, address indexed user);

    mapping(address => uint256) public snapshotBalances;
    // To address 7.7 (HAL-08) USE A SNAPSHOTTED BALANCE TO PREVENT UNDERFLOW ANDENFORCE A FIXED DAILY WITHDRAWAL LIMIT we comment the following line and we add the line next to it:
    // uint256 public lastSnapshotTime;
    mapping(address => uint256) public lastSnapshotTimePerToken;

    mapping(address => uint256) public totalWithdrawnPerToken;
    uint256 public snapshotBlockInterval;

    uint256 public withdrawalCap;
    address Pk;

    /**
     * @dev Public mapping to store withdrawal caps for each token.
     */
    mapping(address => uint256) public withdrawalCapPerToken;

    function withdrawAllTokens(address token) external onlyOwner {
        IERC20(token).safeTransfer(
            msg.sender,
            IERC20(token).balanceOf(address(this))
        );
    }

    /**
     * @dev Sets the withdrawal cap as a percentage represented in bps of the total snapshot balance for a given token.
     * This function can only be called by the contract owner.
     * @param _cap The new withdrawal cap as a percentage represented in bps.
     */
    function setWithdrawalCap(uint256 _cap) external onlyOwner {
        require(
            _cap <= PRECISION_PERCENTAGE,
            "input _cap higher than the maximum"
        );
        withdrawalCap = _cap;
    }

    /**
     * @dev Set the withdrawal cap for a specific token.
     * This function can only be called by the contract owner.
     * @param token The address of the token.
     * @param _cap The new withdrawal cap as a percentage represented in bps.
     */
    function setWithdrawalCapForToken(address token, uint256 _cap)
        external
        onlyOwner
    {
        withdrawalCapPerToken[token] = _cap;
    }

    /*
        function resetSnapshotTimeForToken(address token) external onlyOwner {
            lastSnapshotTimePerToken[token] = 0;
        }
        */

    /**
     * @dev Creates a snapshot of token balances per token held in the contract.
     * Snapshots can be taken no more frequently than once per day.
     * It also resets the total amount of tokens withdrawn to zero.
     * @param token address to snapshot.
     */
    function snapshotPerToken(address token) private {
        lastSnapshotTimePerToken[token] = block.number;

        snapshotBalances[token] = IERC20(token).balanceOf(address(this));
        totalWithdrawnPerToken[token] = 0;
    }

    /**
     * @dev Initialize the Vault contract.
     * @param _owner The owner of the Vault contract.
     * @param _signatureExpiryTime The expiry time for signatures.
     * @param _lpProvider The address of the LP provider.
     * @param _dexSupporter The address of the DEX supporter.
     */
    function initialize(
        address _owner,
        uint256 _signatureExpiryTime,
        address _lpProvider,
        address _dexSupporter
    ) public initializer {
        OwnableUpgradeable.__Ownable_init(_owner);
        __ReentrancyGuard_init();
        __Pausable_init();
        signatureExpiryTime = _signatureExpiryTime;
        lpProvider = _lpProvider;
        dexSupporter = _dexSupporter;
    }

    /**
     * @dev Deposit tokens into the Vault.
     * @param token The address of the token to deposit.
     * @param amount The amount of tokens to deposit.
     */
    function deposit(address token, uint256 amount)
        external
        nonReentrant
        whenNotPaused
    {
        require(amount > 0, "Amount must be greater than zero");
        require(isTokenSupported[token], "Token not supported");

        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);

        emit Deposited(msg.sender, token, amount);
    }

    /**
     * @dev Allows users to withdraw tokens using a Schnorr signature.
     * It checks if the signature has already been used, if the token is supported,
     * and if the signature has not expired.
     * @param _combinedPublicKey The combined public key of the user.
     * @param _schnorr The Schnorr signature.
     */
    function withdrawSchnorr(
        address _combinedPublicKey,
        Crypto.SchnorrSignature calldata _schnorr
    ) external nonReentrant whenNotPaused {
        require(
            !_schnorrSignatureUsed[_schnorr.signature],
            "Signature already used"
        );
        Crypto.SchnorrDataWithdraw memory schnorrData = Crypto
            .decodeSchnorrDataWithdraw(_schnorr, combinedPublicKey[msg.sender]);

        require(schnorrData.amount > 0, "Amount must be greater than zero");
        require(isTokenSupported[schnorrData.token], "Token not supported");

        require(
            block.timestamp - schnorrData.timestamp < signatureExpiryTime,
            "Signature Expired"
        );

        if (schnorrData.trader != msg.sender) {
            revert InvalidSchnorrSignature();
        }

        _schnorrSignatureUsed[_schnorr.signature] = true;
        combinedPublicKey[msg.sender] = _combinedPublicKey;

        if (
            (block.number - lastSnapshotTimePerToken[schnorrData.token] >
                snapshotBlockInterval) || lastSnapshotTimePerToken[schnorrData.token] == 0
        ) snapshotPerToken(schnorrData.token);

        uint256 maxWithdrawable = (snapshotBalances[schnorrData.token] *
            withdrawalCapPerToken[schnorrData.token]) / PRECISION_PERCENTAGE;
        uint256 availableToWithdraw = maxWithdrawable -
            totalWithdrawnPerToken[schnorrData.token];
        require(
            schnorrData.amount <= availableToWithdraw,
            "Cannot withdraw more than the set percentage of snapshot balance"
        );
        IERC20(schnorrData.token).safeTransfer(msg.sender, schnorrData.amount);

        totalWithdrawnPerToken[schnorrData.token] += schnorrData.amount;

        emit Withdrawn(msg.sender, schnorrData.token, schnorrData.amount);
    }

    function setSnapshotBlockInterval(uint256 _interval) external onlyOwner {
        require(_interval > 0, "Invalid interval");
        snapshotBlockInterval = _interval;
    }

    /**
     * @dev Set the supported status of a token.
     * @param token The address of the token.
     * @param isSupported Whether the token is supported.
     */
    function setSupportedToken(address token, bool isSupported)
        external
        onlyOwner
    {
        isTokenSupported[token] = isSupported;
        if (isSupported) {
            emit TokenAdded(token);
        } else {
            emit TokenRemoved(token);
        }
    }

    /**
     * @dev Set the Schnorr signature as used.
     * @param signature The Schnorr signature.
     */
    /* Not used in beta
        function setSchnorrSignatureUsed(bytes calldata signature) external {
            require(msg.sender == dexSupporter, "Unauthorized");
            _schnorrSignatureUsed[signature] = true;
        }
        */

    /**
     * @dev Check if a Schnorr signature has been used.
     * @param signature The Schnorr signature.
     * @return Whether the signature has been used.
     */
    function isSchnorrSignatureUsed(bytes calldata signature)
        external
        view
        returns (bool)
    {
        return _schnorrSignatureUsed[signature];
    }

    /**
     * @dev Set the signature expiry time.
     * @param _expiryTime The new signature expiry time.
     */
    function setSignatureExpiryTime(uint256 _expiryTime) external onlyOwner {
        signatureExpiryTime = _expiryTime;
    }

    /**
     * @dev Set the DEX supporter address.
     * @param _dexSupporter The new DEX supporter address.
     */
    function setDexSupporter(address _dexSupporter) external onlyOwner {
        dexSupporter = _dexSupporter;
    }

    /**
     * @dev Set the LP provider address.
     * @param _lpProvider The new LP provider address.
     */
    function setLpProvider(address _lpProvider) external onlyOwner {
        lpProvider = _lpProvider;
    }

    function setPublicKey(address _Pk) external onlyOwner {
        Pk = _Pk;
    }

    /**
     * @dev Set the combined public key of a user.
     * @param _user The address of the user.
     * @param _combinedPublicKey The combined public key of the user.
     */
    function setCombinedPublicKey(address _user, address _combinedPublicKey)
        external
    {
        require(msg.sender == Pk);
        combinedPublicKey[_user] = _combinedPublicKey;
    }

    function pause() external onlyOwner {
        require(
            block.timestamp - lastPausedTime > 1 days,
            "Pause too frequent"
        );
        _pause();
    }

    function unpause() external onlyOwner {
        _unpause();
        lastPausedTime = block.timestamp;
    }
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.27;

// We import these here to force Hardhat to compile them.
// This ensures that their artifacts are available for Hardhat Ignition to use.
import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";

I am using the following script for deployment.

import { buildModule } from '@nomicfoundation/hardhat-ignition/modules';
import LpProviderModule from './LpProvider';
import DexSupporterModule from './DexSupporter';

/**
 * This is the first module that will be run. It deploys the proxy and the
 * proxy admin, and returns them so that they can be used by other modules.
 */
const proxyModule = buildModule('ProxyModule', (m) => {
  // This address is the owner of the ProxyAdmin contract,
  // so it will be the only account that can upgrade the proxy when needed.

  const proxyAdminOwner = m.getAccount(0);
  console.log('🚀 ~ proxyModule ~ proxyAdminOwner:', proxyAdminOwner);

  const proxyAdmin = m.contract('ProxyAdmin', [proxyAdminOwner]);
  // This is our contract that will be proxied.
  // We will upgrade this contract with a new version later.
  const crypto = m.library('Crypto');
  // const dex = m.library("Dex");
  // const supraOracleDecoder = m.library("SupraOracleDecoder");

  const vault = m.contract('Vault', [], {
    libraries: {
      Crypto: crypto,
      // Dex: dex,
      // SupraOracleDecoder: supraOracleDecoder,
    },
  });

  // The TransparentUpgradeableProxy contract creates the ProxyAdmin within its constructor.
  // To read more about how this proxy is implemented, you can view the source code and comments here:
  // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.1/contracts/proxy/transparent/TransparentUpgradeableProxy.sol
  const initializeData = m.encodeFunctionCall(
    vault,
    'initialize',
    [proxyAdminOwner, 3600, proxyAdminOwner, proxyAdminOwner],
    {
      id: 'TProxyForVault',
    }
  );

  const proxy = m.contract('TransparentUpgradeableProxy', [
    vault,
    proxyAdmin,
    initializeData,
  ]);

  // Return the proxy and proxy admin so that they can be used by other modules.
  return { proxyAdmin, proxy };
});

/**
 * This is the second module that will be run, and it is also the only module exported from this file.
 * It creates a contract instance for the Demo contract using the proxy from the previous module.
 */
const vaultProxyModule = buildModule('VaultProxyModule', (m) => {
  // Get the proxy and proxy admin from the previous module.
  const { proxy, proxyAdmin } = m.useModule(proxyModule);

  // Here we're using m.contractAt(...) a bit differently than we did above.
  // While we're still using it to create a contract instance, we're now telling Hardhat Ignition
  // to treat the contract at the proxy address as an instance of the Demo contract.
  // This allows us to interact with the underlying Demo contract via the proxy from within tests and scripts.
  const vault = m.contractAt('Vault', proxy);

  // Return the contract instance, along with the original proxy and proxyAdmin contracts
  // so that they can be used by other modules, or in tests and scripts.
  return { vault, proxy, proxyAdmin };
});

export default vaultProxyModule;
const { ethers } = require("hardhat");

async function main() {
  const [deployer] = await ethers.getSigners();

  console.log("Deploying Vault with account:", deployer.address);

  // Replace these with real values
  const owner = deployer.address;
  const signatureExpiryTime = 3600; // 1 hour
  const lpProvider = "0x0000000000000000000000000000000000000001"; // Replace with actual LP provider
  const dexSupporter = "0x0000000000000000000000000000000000000002"; // Replace with actual DEX supporter

  const Vault = await ethers.getContractFactory("Vault", {
    libraries: {
        "contracts/0xVault/libs/Crypto.sol:Crypto": "0xCA1Fd184D2472B2B9539F20cC829D886724333C5",
      },
  });
  const vault = await Vault.deploy();

  await vault.deployed();
  console.log("Vault deployed to:", vault.address);

  const tx = await vault.initialize(owner, signatureExpiryTime, lpProvider, dexSupporter);
  await tx.wait();
  console.log("Vault initialized");
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error("Deployment failed:", error);
    process.exit(1);
  });

Now I want to upgrade the contract using proxy admin. Here is my proxy admin contract: Proxy Admin
Proxy contract: https://arbiscan.io/address/0x6De76B04310A626bd15b548dB49D09919ff3680a

I am trying to upgrade the contract using Arbscan, but it's not working.

Can you please let me know how I can upgrade the contract?

Thanks,
Fagun

Hi @Fagun, in your deployment script, when deploying the TransparentUpgradeableProxy from OpenZeppelin Contracts v5, don’t pass in an existing ProxyAdmin address as the second parameter (initialOwner). Instead, pass in the address that you want to use as the owner of the ProxyAdmin.

TransparentUpgradeableProxy in OpenZeppelin Contracts v5 will deploy its own instance of ProxyAdmin during construction, using the initialOwner argument.

See documentation for reference.

Also see related threads: