Function call to proxy contract failing to execute the (logic contract's) function

Hello, I’d really appreciate your help/guidance on this issue.

In summary, I’m testing the proxy contract linked to an upgradable ‘logic’ contract, but upon calling any (logic contract) function on the proxy (eg. the initialize function), I get the error:
“TypeError: proxyInstance.initialize is not a function”

Background
I’m trying to convert a Wallet contract so that it is upgradeable. Following the OZ guides, I’ve updated the Wallet contract to remove constructors, add initialize function etc (to this and the two other contracts it inherits form). And I’ve successfully used Truffle tests (with truffle-upgrade’s deployProxy and upgradeProxy functions) to confirm that the Wallet ‘logic’ contract functions correctly and keeps its state data when it is upgraded to a new version.

I’ve created a WalletProxy contract (inheriting from TransparentUpgradeableProxy) and written some integration Truffle tests, with the aim of testing calls from the WalletProxy to the wallet’s (logic) contract.
In this Truffle test, I first deploy my Wallet (logic) contract and then the WalletProxy contract.

The Issue
In my first ‘integration’ test, I try to call the Wallet’s initialize function via the WalletProxy. However, it fails with the error: “TypeError: proxyInstance.initialize is not a function”. (As do all subsequent test calls to other Wallet ‘logic’ contract functions)

:1234: Code to reproduce

walletProxy_test.js

// Test Scope:
// Tests WalletProxy contract's executution of the wallet (logic)
// contract's functions. Ie. tests the excution of MultiSigWallet's
// functions but with all calls made to the WalletProxy contract.  
// (These tests exclude the use of the WalletProxy's admin functions
// - for admin function tests see WalletProxyAdmin_test.js)

const { deployProxy, upgradeProxy } = require('@openzeppelin/truffle-upgrades')

const WalletProxy = artifacts.require("WalletProxy")
const WalletV1 = artifacts.require("MultiSigWallet")
const WalletV2 = artifacts.require("MultiSigWalletV2")

const truffleAssert = require("truffle-assertions")

let owners
let numTxApprovals

contract("WalletProxy", function(accounts) {

    "use strict"

    let proxyInstance
    let logicInstance
    
    before(async function() {
        // Data for the Wallet Initialisation
        owners = [
            accounts[0],
            accounts[1],
            accounts[2],
        ]
        numTxApprovals = 2

        // Deploy Wallet (logic) contract
        await truffleAssert.passes(
            logicInstance = await WalletV1.new({from: accounts[9]})
        )
        console.log("Deployed Logic Contract - MultiSigWallet:", logicInstance.address)
        
        // Deploy Proxy contract
        await truffleAssert.passes(
            this.walletProxy = await WalletProxy.new(
                logicInstance.address,  // wallet's logic
                accounts[9],            //admin account
                "0x",
                {from: accounts[9]}
            ) 
        )
        console.log("Deployed Proxy Contract - WalletProxy (walletProxy):", this.walletProxy.address)

        proxyInstance = this.walletProxy
    })


    describe("Initializing Wallet", () => {
        it ("should initialize upon the first call", async () => {

        // *** Fails with: "TypeError: proxyInstance.initialize is not a function"
            await truffleAssert.passes(
                await proxyInstance.initialize(
                    owners,
                    numTxApprovals,
                    {from: accounts[0]}
                ),
                "Unable to initialize the wallet!"
            )
            
        })

        it ("should NOT be able to re-initialize wallet with a second call", async () => {

            await truffleAssert.fails(
                await proxyInstance.initialize(owners, numTxApprovals, {from: accounts[1]}),
                "Able to re-initialize wallet (after it was previously initialized)!"
            )
        })
    })

    describe("Wallet's Initial State", () => {

        it ("should have the expected creator", async () => {
            let creator
            await truffleAssert.passes(
                creator = await proxyInstance.getWalletCreator(),
                "Unable to get wallet's creator"
            )
            assert.equal(creator, accounts[0])
        })


// etc, etc

Output from tests:

$ truffle test
Using network 'development'.

Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.

  Contract: WalletProxy

Deployed Logic Contract: 0x1e2aF4e54c8A30a5f9b832BD7809408278BC2505
Deployed Proxy Contract: 0xf4851bc4A14790EbDAfd9d2fC748E4ef1ceeF990 

    Initializing Wallet
      1) should initialize upon the first call
    > No events were emitted
      2) should NOT be able to re-initialize wallet with a second call
    > No events were emitted
    Wallet's Initial State
      3) should have the expected creator
    > No events were emitted
      4) should have the expected owners
    > No events were emitted
      5) should have the expected approvers
    > No events were emitted
      6) should have the expected number of required approvals for a transfer
    > No events were emitted
      7) should initially have no transfer requests
    > No events were emitted

  0 passing (1s)
  7 failing

  1) Contract: WalletProxy
       Initializing Wallet
         should initialize upon the first call:
     TypeError: proxyInstance.initialize is not a function
      at Context.<anonymous> (test/walletProxy_test.js:61:37)
      at processImmediate (internal/timers.js:461:21)

  2) Contract: WalletProxy
       Initializing Wallet
         should NOT be able to re-initialize wallet with a second call:
     TypeError: proxyInstance.initialize is not a function
      at Context.<anonymous> (test/walletProxy_test.js:92:37)
      at processImmediate (internal/timers.js:461:21)

  3) Contract: WalletProxy
       Wallet's Initial State
         should have the expected creator:
     TypeError: proxyInstance.getWalletCreator is not a function
      at Context.<anonymous> (test/walletProxy_test.js:104:47)
      at processImmediate (internal/timers.js:461:21)

  4) Contract: WalletProxy
       Wallet's Initial State
         should have the expected owners:
     TypeError: proxyInstance.getOwners is not a function
      at Context.<anonymous> (test/walletProxy_test.js:113:46)
      at processImmediate (internal/timers.js:461:21)

  5) Contract: WalletProxy
       Wallet's Initial State
         should have the expected approvers:
     TypeError: proxyInstance.getApprovers is not a function
      at Context.<anonymous> (test/walletProxy_test.js:133:49)
      at processImmediate (internal/timers.js:461:21)

  6) Contract: WalletProxy
       Wallet's Initial State
         should have the expected number of required approvals for a transfer:
     TypeError: proxyInstance.getMinApprovals is not a function
      at Context.<anonymous> (test/walletProxy_test.js:153:57)
      at processImmediate (internal/timers.js:461:21)

  7) Contract: WalletProxy
       Wallet's Initial State
         should initially have no transfer requests:
     TypeError: proxyInstance.totalTransferRequests is not a function
      at Context.<anonymous> (test/walletProxy_test.js:166:45)
      at processImmediate (internal/timers.js:461:21)

A call to the WalletProxy’s own function changeAdmin is successful. And also I can call the Wallet logic contract’s initialize function directly (on the logic contract). Specifically, if I comment out the failing test and add the following 3 tests they all pass:

            // ** DEBUG: If I comment out the above failing test, then
            // the following 3 tests all pass:

            await truffleAssert.passes(
                await logicInstance.initialize(owners, numTxApprovals, {from: accounts[0]}),
                "Direct call to logic contract's initialize function failed!"
            )

            await truffleAssert.passes(
                await logicInstance.getWalletCreator({from: accounts[0]}),
                "Direct call to logic contract's getWalletCreator function failed!"
            )
        
            await truffleAssert.passes(
                await proxyInstance.changeAdmin(accounts[8], {from: accounts[9]}),
                "Proxy call failed to change admin!"
            )

I suspect that the issue may be in how I deploy the WalletProxy contract (perhaps how I pass it the logic contract address) under my test’s “before” clause, but can’t see any issue there! Perhaps I’m missing something obvious!??

WalletProxy.sol

pragma solidity 0.8.4;

import "../node_modules/@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";

contract WalletProxy is TransparentUpgradeableProxy {
    
    constructor(address _logic, address admin_, bytes memory _data)
        TransparentUpgradeableProxy(_logic, admin_, _data)
    {
    }
}

MultiSigWallet.sol (First part only, down to the initialize function)

pragma solidity 0.8.4;

import "../node_modules/@openzeppelin/contracts/proxy/utils/Initializable.sol";

import "./MultiOwnable.sol";
import "./Approvable.sol";

contract MultiSigWallet is MultiOwnable, Approvable {

    // STATE VARIABLES
    address internal _walletCreator;

    struct TxRequest {
        address requestor;
        address recipient;
        string reason;
        uint amount;
        uint approvals;
        uint id;    // request id
    }
    
    TxRequest[] internal _txRequests;  // array's index == request id
    mapping (uint => address) internal _txRequestors;
    mapping (address => mapping (uint => bool)) internal _txApprovals;
        // approver => (requestId => approval?)
    
    event DepositReceived(uint amount); 
    event TxRequestCreated(
        uint id,
        uint amount,
        address to,
        address requestor,
        string reason
    );
    event TxApprovalGiven(uint id, uint approvals, address lastApprover);
    event TransferSent(address to, uint amount); 

    // FUNCTIONS
    // Public & External functions
    
    function initialize(address[] memory owners, uint minTxApprovals)
        public
        virtual
        // override
        initializer
    {
        // MultiOwnable.initialize(owners);
        // Approvable.initialize(owners, minTxApprovals);
        MultiOwnable.initializeMultiOwnable(owners);
        Approvable.initializeApprovable(owners, minTxApprovals);

        _walletCreator = msg.sender;
    }
    
    
    function deposit() external payable {
        require (msg.value > 0, "No funds sent to deposit!");
        emit DepositReceived(msg.value);
    }
    
// Etc, etc.

And here are the two classes that the wallet (MultiSigWallet) inherits from:
MultiOwnable.sol

pragma solidity 0.8.4;

import "../node_modules/@openzeppelin/contracts/proxy/utils/Initializable.sol";

contract MultiOwnable is Initializable {
    
    address[] internal _owners;
    mapping (address => bool) internal _ownership;

    modifier onlyAnOwner {
        require(_ownership[msg.sender], "Not an owner!");
        _;
    }
    
    function initializeMultiOwnable(address[] memory owners)
        public
        virtual
        initializer
    {
        for (uint i=0; i < owners.length; i++) {
            require(owners[i] != address(0), "Owner with 0 address!");
            require(!_ownership[owners[i]], "Duplicate owner address!");
            _owners.push(owners[i]);
            _ownership[owners[i]] = true;
        }
        assert(_owners.length == owners.length);
    }
    
    // Functions for Developer testing 
    function getOwners() external view returns (address[] memory owners){
        return _owners;
    }
      
}

Approvable.sol

pragma solidity 0.8.4;

//import "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol"
//import "../node_modules/@openzeppelin/upgrades-core/contracts/Initializable.sol";
import "../node_modules/@openzeppelin/contracts/proxy/utils/Initializable.sol";

contract Approvable is Initializable {
    
    address[] internal _approvers;
    mapping (address => bool) internal _approvership;
    uint internal _minApprovals;

    modifier onlyAnApprover {
         require(_approvership[msg.sender], "Not an approver!");
         _;
    }
    
    function initializeApprovable(address[] memory approvers, uint minApprovals)
        public
        virtual
        initializer
    {
        require(
            minApprovals <= approvers.length,
            "Minimum approvals > approvers!"
        );
        
        for (uint i=0; i < approvers.length; i++) {
            require(approvers[i] != address(0), "Approver has 0 address!");
            require(
                !_approvership[approvers[i]],
                "Duplicate approver address!"
            );
            _approvers.push(approvers[i]);
            _approvership[approvers[i]] = true;
        }
        _minApprovals = minApprovals;

        assert(_approvers.length == approvers.length);
    }

    // Functions for Developer testing 
    function getApprovers() external view returns (address[] memory approvers){
        return _approvers;
    }
    
    function getMinApprovals() external view returns (uint){
        return _minApprovals;
    }
}

:computer: Environment

Truffle v5.3.9 (core: 5.3.9)
Solidity - 0.8.4 (solc-js)
Node v14.15.5
Web3.js v1.3.6
And I’m deploying locally with ganache-cli

I’m still trying to understand (as in my above post) the issue with my Truffle integration testing of my proxy contract together with my logic contract (i.e. My proxy contract is unable to invoke my logic contract functions when they are called against my proxy contract).

I see that all the tutorial ‘Box’ examples use ‘deployProxy’ together with the (Box) logic contract. This is the approach I used with my first set of ‘unit’ tests that confirmed that my wallet logic contract still worked (after refactoring to remove constructors) and the contract is updatable - which all worked fine. However, now I have created my contract (walletProxy), I don’t see any examples of how to integration test my walletProxy with my wallet logic contract. I’m still stuck on getting my Truffle integration test script to check that my own proxy contract (WalletProxy - see code in my post above) calls my wallet logic contract (MultiSigWallet). The examples in ‘box’ tutorial all use ‘deployProxy’ which, as I understand it, creates a proxy for the logic contract and but when I swap this proxy for my actual walletProxy contract, the logic function calls to my walletProxy fail to find/invoke the corresponding logic function…

The ’ Step by Step Tutorial for Truffle’ does show a deployment script ( 2_deploy_box.js) for deploying to a public network but again this uses deployProxy - so (if I understand it correctly) I can’t use this directly to test my own WalletProxy contract (to ensure it routes calls to my logic contract). However, the tutorial then says: " We would normally first deploy our contract to a local test (such as ganache-cli ) and manually interact with it."
I’ve therefore written the following migration script, deployed locally (to ganache-cli) and used Truffle console command line to (again unsuccessfully) call my wallets (logic) contract functions on the walletProxy. See migration script and test calls below.

I now suspect that Truffle doesn’t support full integration testing of a specific proxy contract with a logic contract? Is that the case? Or is there a way to use Truffle and ganache-cli to ‘integration’ test locally (ideally via a Truffle test script)? And if not, does this mean I can only do such ‘integration’ testing after I deploy to an Ethereum testnet ?

Here’s my WalletProxy → Wallet logic integration deployment script (and then the results of truffle CLI function calls):

Any assistance in helping me understand this and how I should proceed with ‘integration’ testing my own proxy and logic contracts would be much appreciated.

2_wallet_logic_&proxy&_admin.js

// Wallet initialization data parameters
const numTxApprovals = 2
let owners

// Deploy the Wallet's Logic contract
const { deployProxy } = require('@openzeppelin/truffle-upgrades')

const MultiSigWallet = artifacts.require("MultiSigWallet")
const WalletProxyAdmin = artifacts.require("WalletProxyAdmin")
const WalletProxy = artifacts.require("WalletProxy")

let logicInstance
let adminInstance
let walletProxyInstance

module.exports = async function (deployer, network, accounts) {

    // Wallet owners
    owners = [
      accounts[0],
      accounts[1],
      accounts[2],
    ]
  
  // Deploy the Wallet's Logic contract
  logicInstance = await deployProxy(
    MultiSigWallet, 
    [owners, numTxApprovals], 
    { deployer }
  )
  console.log("Deployed Logic Contract - MultiSigWallet:", logicInstance.address)

  // Deploy the Wallet's Admin contract
  await deployer.deploy(WalletProxyAdmin)
  adminInstance = await WalletProxyAdmin.deployed()
  console.log("Deployed WalletProxyAdmin:", adminInstance.address)
  
  // Deploy the Wallet's Proxy contract 
  await deployer.deploy(
    WalletProxy,
    logicInstance.address,
    adminInstance.address,
    "0x",
    {from: accounts[0]}
  )
  walletProxyInstance = await WalletProxy.deployed()
  console.log("Deployed WalletProxy:", walletProxyInstance.address)
}

Following deploy to ganache-cli: Failing Logic function calls via Proxy:

Mark$ truffle console
truffle(development)> let proxyInstance = await WalletProxy.deployed()
undefined
truffle(development)> await proxyInstance.getWalletBalance()
Uncaught TypeError: proxyInstance.getWalletBalance is not a function
    at evalmachine.<anonymous>:1:23
truffle(development)> await proxyInstance.getWalletCreator()
Uncaught TypeError: proxyInstance.getWalletCreator is not a function
    at evalmachine.<anonymous>:1:23
truffle(development)> await proxyInstance.totalTransferRequests()
Uncaught TypeError: proxyInstance.totalTransferRequests is not a function
    at evalmachine.<anonymous>:1:23
truffle(development)> 
truffle(development)> let logicInstance = await MultiSigWallet.deployed()
undefined
truffle(development)> await logicInstance.getWalletBalance()
BN { negative: 0, words: [ 0, <1 empty item> ], length: 1, red: null }
truffle(development)> await logicInstance.getWalletCreator()
'0xfC1d4eA100c57A6D975eD8182FaAcFD17871a1e4'
truffle(development)> await logicInstance.totalTransferRequests()
BN { negative: 0, words: [ 0, <1 empty item> ], length: 1, red: null }
truffle(development)> 

I wish I could be more help, but I’m not familiar with the truffle environment.

However I did do a long winded tutorial in this post Tutorial on Using a Gnosis Safe MultiSig with a TimeLock to Upgrade Contracts and use Functions in a Proxy Contract

It’s complicated, but I use a multisig + timelock.

Maybe there is something in there that can guide you.

Many thanks for the reply. Your tutorial is interesting, and I’ll refer back to it once I get to Gnosis safe and a more advanced setup.

It’s just dawned on me, I think my mistake may be that I’m creating my own proxy contract (that inherits from OZ’s TransparentUpgradeableProxy contract). Instead of doing this perhaps I should ALWAYS just use the deployProxy function and not have my own proxy contract at all!
Before I had it fixed in my head that I needed my own proxy contract prior to testnet release. Basically, I think I’ve been overcomplicating it and rather I should just always use deployProxy to create the proxy!?

Is that your understanding? Or can anyone else confirm this point?

I just use the standard proxy that is provided by OZ. I don’t bother with creating my own (well maybe I added a function here and there haha).

Yes 100%.

deployProxy and upgradeProxy are very useful even in a local development environment. They will point out problems in your code.

@Yoshiko / @frangio ,

Okay, many thanks for the help! I’ll rework to eliminate my own proxy contract and instead always use deployProxy & upgradeProxy functions.

1 Like