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)
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;
}
}
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