Interacting with UUPS Upgradeable Contracts in Test Throwing "Contract is not upgrade safe. Use of delegatecall is not allowed"

I am working on a smart contract that interacts with a marketplace that is transparent proxy upgradeable and a lending protocol that is UUPS proxy upgradeable (the contract I am building is non upgradeable).

When I deploy the marketplace and lending protocol for testing purposes in Hardhat/Ethers, I get an error re. the UUPS upgradeable ones:

Error: Contract `ContractName` is not upgrade safe
@openzeppelin/contracts/utils/Address.sol:191: Use of delegatecall is not allowed

The upgradeable contracts already exist on Mainnet and do not have any issues.
Also, they use AddressUpgradeable.sol vs. Address.sol.

Could this issue have to do with the presence of a Transparent proxy and UUPS upgradeable contracts in the same development environment?

This is my contract code:

// SPDX-License-Identifier: MIT

pragma solidity 0.8.15;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/interfaces/IERC721Receiver.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

import "@arcadexyz/v2-contracts/contracts/external/interfaces/ILendingPool.sol";
import "@arcadexyz/v2-contracts/contracts/interfaces/IRepaymentController.sol";
import "@arcadexyz/v2-contracts/contracts/interfaces/ILoanCore.sol";
import "@arcadexyz/v2-contracts/contracts/interfaces/IAssetVault.sol";
import "@arcadexyz/v2-contracts/contracts/interfaces/IPromissoryNote.sol";
import "@arcadexyz/v2-contracts/contracts/InstallmentsCalc.sol";
import "@arcadexyz/v2-contracts/contracts/PromissoryNote.sol";
import "@arcadexyz/v2-contracts/contracts/interfaces/ICallWhitelist.sol";

import "./interfaces/IORsol";
import "./interfaces/ICS.sol";

import "./libraries/SaleLibrary.sol";

contract CS is ICS, IERC721Receiver, InstallmentsCalc, ReentrancyGuard, ERC721Holder {
    using SafeERC20 for IERC20;

    // ======================================== STATE ==================================================

    bytes4 private constant _EIP_1271_MAGIC_VALUE = 0x1626ba7e;

    /* solhint-disable var-name-mixedcase */
    // AAVE Contracts
    // Variable names are in upper case to fulfill IFlashLoanReceiver interface
    ILendingPoolAddressesProvider public immutable override ADDRESSES_PROVIDER;
    ILendingPool public immutable override LENDING_POOL;

    /* solhint-enable var-name-mixedcase */
    address private owner;

    // IERC721Receiver variables
    Error private immutable _error;

    // ========================================== CONSTRUCTOR ===========================================

    constructor(ILendingPoolAddressesProvider _addressesProvider) {
        ADDRESSES_PROVIDER = _addressesProvider;
        LENDING_POOL = ILendingPool(_addressesProvider.getLendingPool());

        owner = msg.sender;

        _error = Error.None;
    }

    
    function verifyLoanData(SaleLibrary.CSD memory csd, SaleLibrary.ContractParams memory contracts)
        internal
        returns (uint256, uint256)
    {
        ....
        
        uint256 _amountToBorrow; // amount to borrow from the flashLoan

        uint256 extraAmount; // amount above and beyond loan amount

        return (_amountToBorrow, extraAmount);
    }

   
    function fulfillCS(
        SaleLibrary.ContractParams memory contracts,
        SaleLibrary.CSD memory csd,
        address _seller,
        address _buyer
    ) external payable returns (bool) {
        // invoke verify loan data verification functionality
        uint256 amountToBorrow;
        uint256 extraAmount;
        (amountToBorrow, extraAmount) = verifyLoanData(csd, contracts);

        address[] memory assets = new address[](1);
        assets[0] = csd.basicOrderParams.offerToken;

        uint256[] memory amounts = new uint256[](1);
        amounts[0] = amountToBorrow;

        uint256[] memory modes = new uint256[](1);
        modes[0] = 0;
...

        bytes memory params = abi.encode(SaleLibrary.FlashLoanParams(contracts, csd, borrowerData));


        LENDING_POOL.flashLoan(
            address(this), // address receiving the funds / implementing IFlashLoanReceiver
            assets, // addresses of the assets being flash-borrowed
            amounts, // amounts amounts being flash-borrowed
            modes, // 0 -> Don't open any debt, just revert if funds can't be transferred from the receiver
            address(this), // address receiving the debt in the case of using on `modes` 1 or 2
            params, // packed params to pass to the receiver as extra information
            0 // referralCode code. referralCode Code
        );
        return true;
    }

    function executeOperation(
        address[] calldata assets,
        uint256[] calldata amounts,
        uint256[] calldata premiums,
        address initiator,
        bytes calldata params
    ) external override nonReentrant returns (bool) {
        require(msg.sender == address(LENDING_POOL), "unknown callback sender");
        require(initiator == address(this), "not initiator");
        return _executeOperation(assets, amounts, premiums, params);
    }

    

    function _executeOperation(
        address[] calldata assets,
        uint256[] calldata amounts,
        uint256[] calldata premiums,
        bytes calldata params
    ) internal returns (bool) {
        SaleLibrary.FlashLoanParams memory decoded = abi.decode(params, (SaleLibrary.FlashLoanParams));
        SaleLibrary.ContractParams memory contracts = decoded.contracts;
        SaleLibrary.CSD memory csd = decoded.csd;
        SaleLibrary.BorrowerMetadata memory borrowerData = decoded.borrowerData;

        // get the external contracts
        OperationContracts memory opContracts = _getContracts(contracts);

        ...
  
        opContracts.oR.fulfillOrder(csd.protocol, csd.targetMarket, csd.recipient, abi.encode(orderParams));

        {
            uint256 csBalance = asset.balanceOf(address(this));
            uint256 amountToRepay;

            amountToRepay = amounts[0] + premiums[0];
            if (amountToRepay > csBalance) {
                uint256 difference = amountToRepay - csBalance;
                asset.transferFrom(borrowerData.borrower, address(this), difference);
            }
        }

        {
            // approve Aave to withdraw owed ERC20 funds after flashLoan execution
            asset.approve(address(LENDING_POOL), amounts[0] + premiums[0]);
        }

        return true;
    }

  
    function _getContracts(SaleLibrary.ContractParams memory contracts) internal returns (OperationContracts memory) {
        return
            OperationContracts({
                ...
            });
    }

    function onERC721Received(
        address operator,
        address from,
        uint256 tokenId,
        bytes memory data
    ) public virtual override(ERC721Holder, IERC721Receiver) returns (bytes4) {
        super.onERC721Received(operator, from, tokenId, data); //call parent hook

        if (_error == Error.RevertWithMessage) {
            revert("ERC721Receiver: reverting");
        } else if (_error == Error.RevertWithoutMessage) {
            //solhint-disable-next-line
            revert();
        } else if (_error == Error.Panic) {
            uint256 a = uint256(0) / uint256(0);
            a;
        }
        emit Received(operator, from, tokenId, data, gasleft());
        return IERC721Receiver.onERC721Received.selector;
    }
}

The imported interface IOR.sol calls the Treasure Marketplace which is a Transparent proxy.

The UUPS proxy imports are:

ILoanCore.sol
IAssetVault.sol

And one inherited by IAssetVault.sol.

This is how I am deploying the upgradeable contracts in my test:
Example of UUPS proxy deployment:

    const VaultFactoryFactory = await hre.ethers.getContractFactory("VaultFactory");
    const vaultFactory = <VaultFactory>await upgrades.deployProxy(
        VaultFactoryFactory,
        [vault.address, whitelist.address],
        {
            kind: "uups",
            initializer: "initialize(address, address)",
            timeout: 0
        },
    );
    await vaultFactory.deployed();

and the transparent proxy:

  const TreasureMarketplace = await ethers.getContractFactory("TreasureMarketplace");
  const marketplaceFee: number = 50;
  const treasureMarketplace = <TreasureMarketplace>(await upgrades.deployProxy(
      TreasureMarketplace,
      [marketplaceFee, owner.address, testERC20.address],
      { kind: "transparent", initializer: "initialize" },
  ));

The test work well and passes when I silence the error by using unsafeAllow: ['delegatecall'], but I cannot rely on that for going to Mainnet.

This is a screenshot of the error:

When using unsafeAllow: ['delegatecall'], with passing tests, I still get this warning:

Screen Shot 2022-10-26 at 7.10.46 PM

1 Like

Hello @here,
Please can anyone share some thoughts on the above ^.
@frangio I am following up on this GH thread.

Transparent and UUPS proxies should be usable at the same time and that should not cause conflicts. (The proxy admin warning is just a reminder that the UUPS proxy is not affected by the proxy admin contract in any way.)

Do any of your upgradeable contracts make use of the CS non-upgradeable contract?

Can you share what your implementation contract looks like for VaultFactory, particularly its imports and inheritance list? Although you mention it uses AddressUpgradeable.sol, it appears as if it has some dependency on Address.sol from the error message.

Hi @ericglau thank you for taking a look.

None of the upgradeable contracts make use of the CS non-upgradeable contract.
I can share the full contract code.

VaultFactory is already on Mainnet and heavily used.
Why would interacting with it in CS in the test environment now cause issues related to Address.sol ?
What are ways in which that can be mitigated? Grateful for any insights...

This is the code to VaultFactory :

This looks like a bug in the Upgrades Plugins where the import is being validated against the wrong library.

The VaultFactory contract has a transitive dependency on Initializable from @openzeppelin/contracts-upgradeable. This is the correct dependency for upgradeable contracts.

But when the plugin does validation, it is also matching against Initializable from @openzeppelin/contracts (due to other contracts importing that version). The validation should match against the one from @openzeppelin/contracts-upgradeable instead.

Initializable (the non-upgradeable version) imports a function from Address, and Address has a delegatecall function and therefore triggers the error.


The root cause is the same as https://github.com/OpenZeppelin/openzeppelin-upgrades/issues/240 since the Address function being imported in Initializable does not contain the delegatecall itself.

But some additional setup in your repo is causing the upgradeable and non-upgradeable library to be mixed up during validations -- this seems to be a separate issue in the plugins which is tracked in https://github.com/OpenZeppelin/openzeppelin-upgrades/issues/263

UPDATE: this is fixed as of @openzeppelin/upgrades-core@1.20.4