0

I have a test project with proxy pattern (fallback/delegate call). The purpose is to have upgradeable contracts. Here is the proxy contract:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.13;

import "hardhat/console.sol";

contract StorageContract {

    address private implementation;

    function setImplementation(address storageimplementation) external
    {
        implementation = storageimplementation;
    }


    fallback() external
    {
        console.log("executing fallback-------");
        delegate(implementation);
    }

    function delegate(address a) internal
    {
        assembly
        {
            calldatacopy(0, 0, calldatasize())

            let result := delegatecall(gas(), a, 0, calldatasize(), 0, 0)

            returndatacopy(0, 0, returndatasize())

            switch result
            case 0
            {
                revert(0, returndatasize())
            }
            default
            {
                return(0, returndatasize())
            }
        }
    }

}

Implementation contract:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.13;

import "hardhat/console.sol";

contract StorageImplementation
{

    function add(uint256 a, uint256 b) public returns (uint256)
    {
        console.log("add function called");
        return a+b;
    }

    function hello() public returns (string memory)
    {
        console.log("hello function called");
        return "hello";
    }

}

Test:

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("Storage", function () {
  it("delegatecall test", async function () {
   const StorageContract = await ethers.getContractFactory("StorageContract");
   const storage = await StorageContract.deploy();
   await storage.deployed();

   const StorageImplementation = await ethers.getContractFactory("StorageImplementation");
   const storageImpl = await StorageImplementation.deploy();
   await storageImpl.deployed();

   storage.setImplementation(storageImpl.address);

   let impl = await storage.getImplementation();
   console.log("impl:" + impl);

    let helloResp = await storage.hello();

    expect(helloResp).to.equal("hello");
  });
});

Fallback function is not executing when I call a function that does not exist in the contract.

After executing tests via npx hardhat test I get an error:

TypeError: storage.hello is not a function

And the log message in fallback is not printed.

I have also tried using receive function and payable modifier with fallback but it didn't help.

I hope I was clear enough. Thanks in advance for any suggestions.

2
  • Does this answer your question? payable() function In solidity May 16, 2022 at 14:14
  • I'm afraid not. I tried with payable modifier, also with receive function as well. From what I've read it is possible that hardhat test checks the abi and returns an error without even giving fallback a chance...
    – Marko
    May 16, 2022 at 14:30

1 Answer 1

2

This fails because the abi of StorageContract has no hello function. So ethers.js doesn't populate the storage object with a hello function, and your call to storage.hello() fails.

You are on the good track given that storage is indeed a proxy for storageImpl, you only need to let ethers "know" about it. One way of solving it is to create an instance of storageImplementation attached to the address of storage :

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("Storage", function () {
  it("delegatecall test", async function () {
    const StorageContract = await ethers.getContractFactory("StorageContract");
    const storage = await StorageContract.deploy();
    await storage.deployed();

    const StorageImplementation = await ethers.getContractFactory(
      "StorageImplementation"
    );
    const storageImpl = await StorageImplementation.deploy();
    await storageImpl.deployed();

    storage.setImplementation(storageImpl.address);

    let impl = await storage.getImplementation();
    console.log("impl:" + impl);

    // Proxy handle : StorageImplementation attached to storage address
    // Trough proxy, you can interact with storage as if it was an instance
    // of StorageImplementation.
    const proxy = await StorageImplementation.attach(storage.address);
    let helloResp = await proxy.hello();

    expect(helloResp).to.equal("hello");
  });
});

I hope this answers your question.

2
  • Thanks, this solves the problem. This is like casting the Storage contract to StorageImplementation. Do you maybe know the equivalent to attach in web3js?
    – Marko
    May 17, 2022 at 8:33
  • 1
    Yeah you could see it that way. For Web3.js an optional address is included in the constructor arguments (web3js.readthedocs.io/en/v1.7.3/…) just give the ABI of the implementation and the address of the proxy. Something like new web3.eth.Contract(IMPLEMENTATION_ABI, proxy.address) should work.
    – hroussille
    May 17, 2022 at 8:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.