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.

Marko
  • 105
  • 3

1 Answers1

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.

hroussille
  • 7,661
  • 2
  • 6
  • 29
  • 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 '22 at 08:33
  • 1
    Yeah you could see it that way. For Web3.js an optional address is included in the constructor arguments (https://web3js.readthedocs.io/en/v1.7.3/web3-eth-contract.html#new-contract) 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 '22 at 08:50